Add standalone Paperclip task exporter by noverhead-germany · Pull Request #17077 · paperclipai/paperclip · GitHub
Skip to content

Add standalone Paperclip task exporter - #17077

Draft
noverhead-germany wants to merge 1 commit into
paperclipai:masterfrom
noverhead-germany:codex/paperclip-task-exporter
Draft

noverhead-germany wants to merge 1 commit into
paperclipai:masterfrom
noverhead-germany:codex/paperclip-task-exporter

Conversation

@noverhead-germany

Copy link
Copy Markdown

Thinking Path

  • Paperclip manages AI agent work through tasks and run records.
  • Operators sometimes need an offline copy of one task and its execution history.
  • The API already exposes task, comment, activity, document, attachment, and run data.
  • A small command line tool can collect those records without changing the Paperclip server.
  • This pull request adds a self-contained Windows exporter.
  • The archive records API gaps and replaces old output only after writing a complete new ZIP.

Linked Issues or Issue Description

Problem or motivation
Operators need a portable ZIP of one task and its related records.

Proposed solution
Provide a standalone .NET 10 Windows executable that reads local Paperclip credentials and exports task data to a ZIP archive.

Alternatives considered
Manual API requests require repeated setup and do not produce a single archive.

What Changed

  • Add a self-contained .NET 10 console exporter.
  • Read the token from the local Paperclip auth file.
  • Export task details, comments, activity, documents, attachments, runs, events, and logs.
  • Record inaccessible sections in ERRORS.txt.
  • Replace an existing ZIP only after the new archive is complete.
  • Add build and run instructions.

Verification

  • dotnet publish tools/PaperclipExport/PaperclipExport.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
  • Exported NOV-88 from a live Paperclip instance.
  • Opened the resulting ZIP and confirmed task data, comments, activity, four runs, run events, and run logs.
  • Repeated the export and confirmed the existing ZIP was replaced.

Risks

  • Low code risk. The API base URL and company ID are fixed for the current deployment.
  • The exporter depends on the local auth file format and Paperclip API routes.

Model Used

  • OpenAI Codex, GPT-6, tool use and code execution.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes/Closes/Refs OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

@commitperclip

commitperclip Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 0/5

[Medium impact] The exporter copies one task's records and run history into a local archive.

Do not merge until credentials are protected and exports reliably preserve the requested task and its records.

Findings

  1. P1 Security Login token travels unencrypted ▶
  2. P1 Other users cannot load credentials ▶
  3. P1 Tasks beyond page one disappear ▶
  4. P1 Longer histories are cut short ▶
  5. P1 Attached files are missing ▶
  6. P1 Failed exports overwrite useful copies ▶
Fix with agent prompt
### Issue 1
tools/PaperclipExport/Program.cs:15-16
The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.

**How this was verified:** Every API request uses the HTTP `BaseUrl` with an `Authorization: Bearer` header, and the server accepts the token until it expires or is revoked.

### Issue 2
tools/PaperclipExport/Program.cs:7
`ConfigPath` points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at `File.ReadAllTextAsync` before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.

```suggestion
var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json");
```

### Issue 3
tools/PaperclipExport/Program.cs:22-24
Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch `/api/issues/{ticket}` directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.

### Issue 4
tools/PaperclipExport/Program.cs:33
Comments, run events, and logs are saved after just one page:
- Tasks with more than 100 comments lose later comments.
- The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.

These successful responses add no gap to `ERRORS.txt`. Follow the comment cursor, event `seq`, and log `nextOffset` until all pages have been saved.

### Issue 5
tools/PaperclipExport/Program.cs:32
Attachment downloads require an absolute `url`, but the API returns a relative `contentPath` instead. The download branch therefore skips every attachment without recording an error. Fetch the returned `contentPath` and use `originalFilename` for the archive entry.

### Issue 6
tools/PaperclipExport/Program.cs:37
A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only `ERRORS.txt`, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds a standalone .NET 10 exporter that collects task records into a ZIP.

  • Operators can package a task and its run history into a ZIP.

noverhead-germany explicitly states that the API base URL and company ID are fixed for the current deployment. Their lack of configurability was not reported.

Reviews (1) · Last reviewed commit: "Add standalone Paperclip task exporter" · Reviewed by Greptile

Comment on lines +15 to +16
using var http = new HttpClient { BaseAddress = new Uri(BaseUrl), Timeout = TimeSpan.FromSeconds(45) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Login token travels unencrypted

The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.

How this was verified: Every API request uses the HTTP BaseUrl with an Authorization: Bearer header, and the server accepts the token until it expires or is revoked.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 15-16

Comment:
**Login token travels unencrypted**

The exporter sends its bearer token and task data over plain HTTP to a LAN address. If someone can observe that traffic, they can read the token and reuse its access. Use HTTPS for non-loopback connections.

**How this was verified:** Every API request uses the HTTP `BaseUrl` with an `Authorization: Bearer` header, and the server accepts the token until it expires or is revoked.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

using System.Text.RegularExpressions;

const string BaseUrl = "http://192.168.1.21:3100";
const string ConfigPath = @"C:\Users\NOverhead\.paperclip\auth.json";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Other users cannot load credentials

ConfigPath points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at File.ReadAllTextAsync before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.

Suggested change
const string ConfigPath = @"C:\Users\NOverhead\.paperclip\auth.json";
var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json");
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 7

Comment:
**Other users cannot load credentials**

`ConfigPath` points to one person's Windows profile, rather than the current user's profile as the README promises. Another Windows user with valid local credentials will fail at `File.ReadAllTextAsync` before any export starts. Resolve the current user's home directory or accept an explicit auth-file path.

```suggestion
var ConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".paperclip", "auth.json");
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +22 to +24
var list=await JsonGet($"/api/companies/{CompanyId}/issues?limit=500&offset=0", "Issue lookup"); JsonElement issue=default;
if(list is not null){var root=list.RootElement;IEnumerable<JsonElement> items=root.ValueKind==JsonValueKind.Array?root.EnumerateArray():root.TryGetProperty("items",out var it)&&it.ValueKind==JsonValueKind.Array?it.EnumerateArray():[];issue=items.FirstOrDefault(x=>x.TryGetProperty("identifier",out var v)&&string.Equals(v.GetString(),ticket,StringComparison.OrdinalIgnoreCase));}
if(issue.ValueKind!=JsonValueKind.Object){gaps.Add($"Issue {ticket} was not found in the first 500 company issues.");}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Tasks beyond page one disappear

Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch /api/issues/{ticket} directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 22-24

Comment:
**Tasks beyond page one disappear**

Task lookup stops after the first 500 company issues. Any existing task outside that page is treated as missing and gets no task data in its archive. Fetch `/api/issues/{ticket}` directly; the server already resolves issue identifiers on that route. This also avoids downloading hundreds of unrelated tasks.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

await Save("documents.json",await Get($"/api/issues/{id}/documents","Documents"));
var attachments=await JsonGet($"/api/issues/{id}/attachments","Attachments");
if(attachments is not null){await Save("attachments.json",JsonSerializer.SerializeToUtf8Bytes(attachments.RootElement,new JsonSerializerOptions{WriteIndented=true}));IEnumerable<JsonElement> arr=attachments.RootElement.ValueKind==JsonValueKind.Array?attachments.RootElement.EnumerateArray():[];var ix=0;foreach(var a in arr){ix++;if(a.TryGetProperty("url",out var u)&&u.ValueKind==JsonValueKind.String&&Uri.TryCreate(u.GetString(),UriKind.Absolute,out var uri)&&uri.Host==new Uri(BaseUrl).Host)await Save($"attachments/{ix}-{Path.GetFileName(uri.LocalPath)}",await Get(uri.PathAndQuery,$"Attachment {ix} file"));}}
var runs=await JsonGet($"/api/issues/{id}/runs","Agent runs for logs"); if(runs is not null){IEnumerable<JsonElement> rs=runs.RootElement.ValueKind==JsonValueKind.Array?runs.RootElement.EnumerateArray():runs.RootElement.TryGetProperty("items",out var ri)&&ri.ValueKind==JsonValueKind.Array?ri.EnumerateArray():[];foreach(var run in rs){if(!(run.TryGetProperty("runId",out var rv)||run.TryGetProperty("id",out rv))||rv.GetString() is not string rid)continue;await Save($"runs/{rid}/events.json",await Get($"/api/heartbeat-runs/{rid}/events?afterSeq=0&limit=10000",$"Run {rid} events"));await Save($"runs/{rid}/log.json",await Get($"/api/heartbeat-runs/{rid}/log?offset=0&limitBytes=10000000",$"Run {rid} log"));}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Longer histories are cut short

Comments, run events, and logs are saved after just one page:

  • Tasks with more than 100 comments lose later comments.
  • The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.

These successful responses add no gap to ERRORS.txt. Follow the comment cursor, event seq, and log nextOffset until all pages have been saved.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 33

Comment:
**Longer histories are cut short**

Comments, run events, and logs are saved after just one page:
- Tasks with more than 100 comments lose later comments.
- The server caps events at 1,000 and logs at 1 MiB, despite the larger limits requested here.

These successful responses add no gap to `ERRORS.txt`. Follow the comment cursor, event `seq`, and log `nextOffset` until all pages have been saved.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

await Save("runs.json",await Get($"/api/issues/{id}/runs","Agent runs"));
await Save("documents.json",await Get($"/api/issues/{id}/documents","Documents"));
var attachments=await JsonGet($"/api/issues/{id}/attachments","Attachments");
if(attachments is not null){await Save("attachments.json",JsonSerializer.SerializeToUtf8Bytes(attachments.RootElement,new JsonSerializerOptions{WriteIndented=true}));IEnumerable<JsonElement> arr=attachments.RootElement.ValueKind==JsonValueKind.Array?attachments.RootElement.EnumerateArray():[];var ix=0;foreach(var a in arr){ix++;if(a.TryGetProperty("url",out var u)&&u.ValueKind==JsonValueKind.String&&Uri.TryCreate(u.GetString(),UriKind.Absolute,out var uri)&&uri.Host==new Uri(BaseUrl).Host)await Save($"attachments/{ix}-{Path.GetFileName(uri.LocalPath)}",await Get(uri.PathAndQuery,$"Attachment {ix} file"));}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Attached files are missing

Attachment downloads require an absolute url, but the API returns a relative contentPath instead. The download branch therefore skips every attachment without recording an error. Fetch the returned contentPath and use originalFilename for the archive entry.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 32

Comment:
**Attached files are missing**

Attachment downloads require an absolute `url`, but the API returns a relative `contentPath` instead. The download branch therefore skips every attachment without recording an error. Fetch the returned `contentPath` and use `originalFilename` for the archive entry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed exports overwrite useful copies

A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only ERRORS.txt, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/PaperclipExport/Program.cs
Line: 37

Comment:
**Failed exports overwrite useful copies**

A failed task lookup still replaces the existing ZIP and returns success. For example, a timeout or expired token creates an archive containing only `ERRORS.txt`, then overwrites a previously useful export. Stop with a nonzero exit code and preserve the existing archive when no task was fetched. Optional section gaps can still be recorded separately.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant