promptarc 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +33 -25
  2. package/dist/bin.js +561 -45
  3. package/dist/share-direct.js +360 -0
  4. package/dist/web/.next/BUILD_ID +1 -1
  5. package/dist/web/.next/app-build-manifest.json +86 -79
  6. package/dist/web/.next/app-path-routes-manifest.json +17 -16
  7. package/dist/web/.next/build-manifest.json +2 -2
  8. package/dist/web/.next/prerender-manifest.json +14 -14
  9. package/dist/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  10. package/dist/web/.next/server/app/api/admin/backfill-ai/route_client-reference-manifest.js +1 -1
  11. package/dist/web/.next/server/app/api/admin/backfill-search/route_client-reference-manifest.js +1 -1
  12. package/dist/web/.next/server/app/api/ai/context/route.js +31 -0
  13. package/dist/web/.next/server/app/api/ai/context/route.js.nft.json +1 -0
  14. package/dist/web/.next/server/app/api/ai/context/route_client-reference-manifest.js +1 -0
  15. package/dist/web/.next/server/app/api/ai/pr-summary/route_client-reference-manifest.js +1 -1
  16. package/dist/web/.next/server/app/api/ai/pr-summary-local/route_client-reference-manifest.js +1 -1
  17. package/dist/web/.next/server/app/api/ai/process/route_client-reference-manifest.js +1 -1
  18. package/dist/web/.next/server/app/api/cli/download/route_client-reference-manifest.js +1 -1
  19. package/dist/web/.next/server/app/api/cli/feedback/route_client-reference-manifest.js +1 -1
  20. package/dist/web/.next/server/app/api/cli/init/route_client-reference-manifest.js +1 -1
  21. package/dist/web/.next/server/app/api/cli/my-shares/route_client-reference-manifest.js +1 -1
  22. package/dist/web/.next/server/app/api/cli/poll/route_client-reference-manifest.js +1 -1
  23. package/dist/web/.next/server/app/api/cli/project-settings/route_client-reference-manifest.js +1 -1
  24. package/dist/web/.next/server/app/api/cli/share-mutate/route_client-reference-manifest.js +1 -1
  25. package/dist/web/.next/server/app/api/cli/sync-list/route_client-reference-manifest.js +1 -1
  26. package/dist/web/.next/server/app/api/local/project-sessions/route_client-reference-manifest.js +1 -1
  27. package/dist/web/.next/server/app/api/local/watch/route_client-reference-manifest.js +1 -1
  28. package/dist/web/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  29. package/dist/web/.next/server/app/api/search-index/route_client-reference-manifest.js +1 -1
  30. package/dist/web/.next/server/app/api/share/download/[slug]/route_client-reference-manifest.js +1 -1
  31. package/dist/web/.next/server/app/api/share/route_client-reference-manifest.js +1 -1
  32. package/dist/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
  33. package/dist/web/.next/server/app/auth/callback/route_client-reference-manifest.js +1 -1
  34. package/dist/web/.next/server/app/auth/signout/route_client-reference-manifest.js +1 -1
  35. package/dist/web/.next/server/app/blog/ai-coding-agents-2026/page_client-reference-manifest.js +1 -1
  36. package/dist/web/.next/server/app/blog/page_client-reference-manifest.js +1 -1
  37. package/dist/web/.next/server/app/cli/login/page_client-reference-manifest.js +1 -1
  38. package/dist/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  39. package/dist/web/.next/server/app/feedback/page_client-reference-manifest.js +1 -1
  40. package/dist/web/.next/server/app/my-projects/[projectKey]/page_client-reference-manifest.js +1 -1
  41. package/dist/web/.next/server/app/my-projects/page_client-reference-manifest.js +1 -1
  42. package/dist/web/.next/server/app/my-shares/page_client-reference-manifest.js +1 -1
  43. package/dist/web/.next/server/app/opengraph-image/route_client-reference-manifest.js +1 -1
  44. package/dist/web/.next/server/app/page.js +1 -1
  45. package/dist/web/.next/server/app/page_client-reference-manifest.js +1 -1
  46. package/dist/web/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  47. package/dist/web/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  48. package/dist/web/.next/server/app/r/[slug]/opengraph-image/route_client-reference-manifest.js +1 -1
  49. package/dist/web/.next/server/app/r/[slug]/page.js +1 -1
  50. package/dist/web/.next/server/app/r/[slug]/page_client-reference-manifest.js +1 -1
  51. package/dist/web/.next/server/app/robots.txt/route.js.nft.json +1 -1
  52. package/dist/web/.next/server/app/robots.txt/route_client-reference-manifest.js +1 -1
  53. package/dist/web/.next/server/app/search/page_client-reference-manifest.js +1 -1
  54. package/dist/web/.next/server/app/sessions/[projectId]/[sessionId]/page_client-reference-manifest.js +1 -1
  55. package/dist/web/.next/server/app/sign-in/page_client-reference-manifest.js +1 -1
  56. package/dist/web/.next/server/app/sitemap.xml/route.js.nft.json +1 -1
  57. package/dist/web/.next/server/app/sitemap.xml/route_client-reference-manifest.js +1 -1
  58. package/dist/web/.next/server/app/sitemap.xml.body +8 -8
  59. package/dist/web/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  60. package/dist/web/.next/server/app-paths-manifest.json +17 -16
  61. package/dist/web/.next/server/functions-config-manifest.json +10 -7
  62. package/dist/web/.next/server/pages/500.html +1 -1
  63. package/dist/web/.next/server/server-reference-manifest.js +1 -1
  64. package/dist/web/.next/server/server-reference-manifest.json +1 -1
  65. package/dist/web/.next/static/LF69v3WA6LbEAkvRHNqfG/_buildManifest.js +1 -0
  66. package/dist/web/.next/static/chunks/app/_not-found/page-0f1123e35eb8f046.js +1 -0
  67. package/dist/web/.next/static/chunks/app/api/admin/backfill-ai/route-0f1123e35eb8f046.js +1 -0
  68. package/dist/web/.next/static/chunks/app/api/admin/backfill-search/route-0f1123e35eb8f046.js +1 -0
  69. package/dist/web/.next/static/chunks/app/api/ai/context/route-0f1123e35eb8f046.js +1 -0
  70. package/dist/web/.next/static/chunks/app/api/ai/pr-summary/route-0f1123e35eb8f046.js +1 -0
  71. package/dist/web/.next/static/chunks/app/api/ai/pr-summary-local/route-0f1123e35eb8f046.js +1 -0
  72. package/dist/web/.next/static/chunks/app/api/ai/process/route-0f1123e35eb8f046.js +1 -0
  73. package/dist/web/.next/static/chunks/app/api/cli/download/route-0f1123e35eb8f046.js +1 -0
  74. package/dist/web/.next/static/chunks/app/api/cli/feedback/route-0f1123e35eb8f046.js +1 -0
  75. package/dist/web/.next/static/chunks/app/api/cli/init/route-0f1123e35eb8f046.js +1 -0
  76. package/dist/web/.next/static/chunks/app/api/cli/my-shares/route-0f1123e35eb8f046.js +1 -0
  77. package/dist/web/.next/static/chunks/app/api/cli/poll/route-0f1123e35eb8f046.js +1 -0
  78. package/dist/web/.next/static/chunks/app/api/cli/project-settings/route-0f1123e35eb8f046.js +1 -0
  79. package/dist/web/.next/static/chunks/app/api/cli/share-mutate/route-0f1123e35eb8f046.js +1 -0
  80. package/dist/web/.next/static/chunks/app/api/cli/sync-list/route-0f1123e35eb8f046.js +1 -0
  81. package/dist/web/.next/static/chunks/app/api/local/project-sessions/route-0f1123e35eb8f046.js +1 -0
  82. package/dist/web/.next/static/chunks/app/api/local/watch/route-0f1123e35eb8f046.js +1 -0
  83. package/dist/web/.next/static/chunks/app/api/search/route-0f1123e35eb8f046.js +1 -0
  84. package/dist/web/.next/static/chunks/app/api/search-index/route-0f1123e35eb8f046.js +1 -0
  85. package/dist/web/.next/static/chunks/app/api/share/download/[slug]/route-0f1123e35eb8f046.js +1 -0
  86. package/dist/web/.next/static/chunks/app/api/share/route-0f1123e35eb8f046.js +1 -0
  87. package/dist/web/.next/static/chunks/app/api/upload/route-0f1123e35eb8f046.js +1 -0
  88. package/dist/web/.next/static/chunks/app/auth/callback/route-0f1123e35eb8f046.js +1 -0
  89. package/dist/web/.next/static/chunks/app/auth/signout/route-0f1123e35eb8f046.js +1 -0
  90. package/dist/web/.next/static/chunks/app/blog/ai-coding-agents-2026/page-0f1123e35eb8f046.js +1 -0
  91. package/dist/web/.next/static/chunks/app/opengraph-image/route-0f1123e35eb8f046.js +1 -0
  92. package/dist/web/.next/static/chunks/app/{page-f7b69fa944b46c20.js → page-95394e3c6370084f.js} +1 -1
  93. package/dist/web/.next/static/chunks/app/r/[slug]/opengraph-image/route-0f1123e35eb8f046.js +1 -0
  94. package/dist/web/.next/static/chunks/app/robots.txt/route-0f1123e35eb8f046.js +1 -0
  95. package/dist/web/.next/static/chunks/app/sitemap.xml/route-0f1123e35eb8f046.js +1 -0
  96. package/dist/web/.next/static/css/c76bb7513858c50c.css +3 -0
  97. package/package.json +1 -1
  98. package/dist/web/.next/static/ABHGCNY7MSdkh00Z4Y8wS/_buildManifest.js +0 -1
  99. package/dist/web/.next/static/chunks/app/_not-found/page-fd6592d30c58fa5e.js +0 -1
  100. package/dist/web/.next/static/chunks/app/api/admin/backfill-ai/route-fd6592d30c58fa5e.js +0 -1
  101. package/dist/web/.next/static/chunks/app/api/admin/backfill-search/route-fd6592d30c58fa5e.js +0 -1
  102. package/dist/web/.next/static/chunks/app/api/ai/pr-summary/route-fd6592d30c58fa5e.js +0 -1
  103. package/dist/web/.next/static/chunks/app/api/ai/pr-summary-local/route-fd6592d30c58fa5e.js +0 -1
  104. package/dist/web/.next/static/chunks/app/api/ai/process/route-fd6592d30c58fa5e.js +0 -1
  105. package/dist/web/.next/static/chunks/app/api/cli/download/route-fd6592d30c58fa5e.js +0 -1
  106. package/dist/web/.next/static/chunks/app/api/cli/feedback/route-fd6592d30c58fa5e.js +0 -1
  107. package/dist/web/.next/static/chunks/app/api/cli/init/route-fd6592d30c58fa5e.js +0 -1
  108. package/dist/web/.next/static/chunks/app/api/cli/my-shares/route-fd6592d30c58fa5e.js +0 -1
  109. package/dist/web/.next/static/chunks/app/api/cli/poll/route-fd6592d30c58fa5e.js +0 -1
  110. package/dist/web/.next/static/chunks/app/api/cli/project-settings/route-fd6592d30c58fa5e.js +0 -1
  111. package/dist/web/.next/static/chunks/app/api/cli/share-mutate/route-fd6592d30c58fa5e.js +0 -1
  112. package/dist/web/.next/static/chunks/app/api/cli/sync-list/route-fd6592d30c58fa5e.js +0 -1
  113. package/dist/web/.next/static/chunks/app/api/local/project-sessions/route-fd6592d30c58fa5e.js +0 -1
  114. package/dist/web/.next/static/chunks/app/api/local/watch/route-fd6592d30c58fa5e.js +0 -1
  115. package/dist/web/.next/static/chunks/app/api/search/route-fd6592d30c58fa5e.js +0 -1
  116. package/dist/web/.next/static/chunks/app/api/search-index/route-fd6592d30c58fa5e.js +0 -1
  117. package/dist/web/.next/static/chunks/app/api/share/download/[slug]/route-fd6592d30c58fa5e.js +0 -1
  118. package/dist/web/.next/static/chunks/app/api/share/route-fd6592d30c58fa5e.js +0 -1
  119. package/dist/web/.next/static/chunks/app/api/upload/route-fd6592d30c58fa5e.js +0 -1
  120. package/dist/web/.next/static/chunks/app/auth/callback/route-fd6592d30c58fa5e.js +0 -1
  121. package/dist/web/.next/static/chunks/app/auth/signout/route-fd6592d30c58fa5e.js +0 -1
  122. package/dist/web/.next/static/chunks/app/blog/ai-coding-agents-2026/page-fd6592d30c58fa5e.js +0 -1
  123. package/dist/web/.next/static/chunks/app/opengraph-image/route-fd6592d30c58fa5e.js +0 -1
  124. package/dist/web/.next/static/chunks/app/r/[slug]/opengraph-image/route-fd6592d30c58fa5e.js +0 -1
  125. package/dist/web/.next/static/chunks/app/robots.txt/route-fd6592d30c58fa5e.js +0 -1
  126. package/dist/web/.next/static/chunks/app/sitemap.xml/route-fd6592d30c58fa5e.js +0 -1
  127. package/dist/web/.next/static/css/b3ccdd7835e6edda.css +0 -3
  128. /package/dist/web/.next/static/{ABHGCNY7MSdkh00Z4Y8wS → LF69v3WA6LbEAkvRHNqfG}/_ssgManifest.js +0 -0
package/dist/bin.js CHANGED
@@ -11,16 +11,19 @@
11
11
  * promptarc login Run the auth flow without booting the server
12
12
  * promptarc logout Delete the saved auth token
13
13
  * promptarc whoami Print the signed-in account
14
+ * promptarc pr Generate a PR description from the current session
15
+ * promptarc pull Download a shared session for resume
14
16
  * promptarc --version Print the package version
15
17
  */
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
17
19
  import { homedir } from 'node:os';
18
- import { dirname, join, resolve } from 'node:path';
20
+ import { basename, dirname, join, resolve } from 'node:path';
19
21
  import { fileURLToPath } from 'node:url';
20
22
  import { spawn } from 'node:child_process';
21
23
  import { createServer } from 'node:net';
22
24
  import { runLogin, loadAuth, deleteAuth } from './auth.js';
23
25
  import { startSyncLoop } from './sync.js';
26
+ import { buildReplayFromJsonl } from './share-direct.js';
24
27
  const __filename = fileURLToPath(import.meta.url);
25
28
  const __dirname = dirname(__filename);
26
29
  const VERSION = readJson(join(__dirname, '..', 'package.json')).version ?? '0.0.0';
@@ -54,10 +57,25 @@ async function main() {
54
57
  console.log(`Signed in as ${auth.email ?? auth.userId}${auth.login ? ` (@${auth.login})` : ''}`);
55
58
  return;
56
59
  }
60
+ if (cmd === 'context') {
61
+ await runContext(args.slice(1));
62
+ return;
63
+ }
57
64
  if (cmd === 'pr') {
58
65
  await runPR(args.slice(1));
59
66
  return;
60
67
  }
68
+ if (cmd === 'share') {
69
+ const sessionArg = args[1];
70
+ if (!sessionArg) {
71
+ console.error('Usage: promptarc share <session-id>');
72
+ console.error(' Share a local session to the cloud as private.');
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ await runShare(sessionArg, args.slice(2));
77
+ return;
78
+ }
61
79
  if (cmd === 'pull') {
62
80
  const slug = args[1];
63
81
  if (!slug) {
@@ -85,8 +103,10 @@ COMMANDS
85
103
  login Sign in to your promptarc account via GitHub
86
104
  logout Delete the saved auth token
87
105
  whoami Print the currently signed-in account
106
+ context What do I need to know? Past sessions for this project
88
107
  pr Generate a PR description from the current session
89
108
  pr --share Generate + share session as private (includes link)
109
+ share <id> Share a local session to the cloud (private by default)
90
110
  pull <slug> Download a shared session into ~/.claude/ for resume
91
111
  --version Print the CLI version
92
112
  --help Show this message
@@ -95,13 +115,515 @@ LEARN MORE
95
115
  https://promptarc.dev
96
116
  `);
97
117
  }
98
- async function runPR(args) {
118
+ async function runContext(args) {
119
+ const cwd = process.cwd();
120
+ const query = args.filter((a) => !a.startsWith('--')).join(' ');
121
+ const filesFlag = args.includes('--files');
122
+ const cwdParts = cwd.split('/').filter(Boolean);
123
+ const projectName = cwdParts[cwdParts.length - 1] ?? '';
124
+ console.log(`promptarc context`);
125
+ console.log(` ▸ project: ${projectName}`);
126
+ // 1. Detect recently changed files (git + find fallback).
127
+ let files = [];
128
+ if (filesFlag) {
129
+ try {
130
+ const { execSync } = await import('node:child_process');
131
+ // Try git first: staged + last 3 commits.
132
+ const gitFiles = execSync('git diff --name-only HEAD~3 HEAD 2>/dev/null; git diff --name-only --cached 2>/dev/null; git diff --name-only 2>/dev/null', { cwd, encoding: 'utf8' });
133
+ files = [...new Set(gitFiles.split('\n').filter(Boolean))].slice(0, 15);
134
+ }
135
+ catch { /* not a git repo */ }
136
+ // Fallback: recently modified files.
137
+ if (files.length === 0) {
138
+ try {
139
+ const { execSync } = await import('node:child_process');
140
+ const found = execSync('find . -maxdepth 3 -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.py" -o -name "*.rs" -o -name "*.go" | head -15', { cwd, encoding: 'utf8' });
141
+ files = found.split('\n').filter(Boolean).map((f) => f.replace(/^\.\//, ''));
142
+ }
143
+ catch { /* no find */ }
144
+ }
145
+ }
146
+ if (files.length > 0) {
147
+ console.log(` ▸ files: ${files.slice(0, 5).join(', ')}${files.length > 5 ? ` +${files.length - 5} more` : ''}`);
148
+ }
149
+ // 2. Local scan: find sessions across all three agents for this project.
150
+ console.log(` ▸ scanning local sessions...`);
151
+ const localSessions = scanLocalSessions(cwd, projectName);
152
+ if (localSessions.length > 0) {
153
+ const lastActive = fmtRelativeTime(localSessions[0].mtime);
154
+ console.log(`\n ${projectName} · ${localSessions.length} session${localSessions.length === 1 ? '' : 's'} · last active ${lastActive}`);
155
+ }
156
+ // 3. Cloud search + AI briefing (requires auth).
99
157
  const auth = loadAuth();
158
+ let cloudSessionsFound = false;
159
+ if (auth) {
160
+ console.log(`\n ▸ searching cloud archive...`);
161
+ const siteUrl = process.env.PROMPTARC_SITE_URL ?? 'https://promptarc.dev';
162
+ // Build local context to send to the API for AI briefing.
163
+ const localContext = localSessions.slice(0, 3).map((s) => {
164
+ const prompt = s.firstPrompt
165
+ ? s.firstPrompt.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 300)
166
+ : '';
167
+ return `[${s.agent}] ${prompt} (${s.messageCount} messages)`;
168
+ }).join('\n');
169
+ try {
170
+ const res = await fetch(`${siteUrl}/api/ai/context`, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'content-type': 'application/json',
174
+ authorization: `Bearer ${auth.token}`,
175
+ },
176
+ body: JSON.stringify({
177
+ cwd,
178
+ files,
179
+ query,
180
+ localContext,
181
+ }),
182
+ });
183
+ if (res.ok) {
184
+ const json = (await res.json());
185
+ if (json.count > 0) {
186
+ cloudSessionsFound = true;
187
+ console.log(` ${json.count} cloud session${json.count === 1 ? '' : 's'} found.\n`);
188
+ }
189
+ else {
190
+ console.log(` No cloud sessions found.`);
191
+ if (localSessions.length > 0) {
192
+ console.log(` Share sessions to unlock AI briefing from any machine.`);
193
+ }
194
+ }
195
+ // Resume command — pick up the exact session.
196
+ if (localSessions.length > 0) {
197
+ const latest = localSessions[0];
198
+ const agentCmd = latest.agent === 'codex'
199
+ ? 'codex'
200
+ : latest.agent === 'cursor'
201
+ ? 'cursor'
202
+ : 'claude';
203
+ const shortId = latest.sessionId ?? basename(latest.path, '.jsonl');
204
+ console.log(`\n Resume last session:`);
205
+ console.log(` ${agentCmd} --resume ${shortId}`);
206
+ }
207
+ // AI briefing — the actionable part.
208
+ if (json.aiBriefing) {
209
+ if (json.aiBriefing.unfinished) {
210
+ console.log(`\n What's unfinished:`);
211
+ for (const line of json.aiBriefing.unfinished.split('\n')) {
212
+ const trimmed = line.trim();
213
+ if (trimmed)
214
+ console.log(` ${trimmed}`);
215
+ }
216
+ }
217
+ if (json.aiBriefing.resumePrompt) {
218
+ console.log(`\n Resume prompt (paste into your agent):`);
219
+ console.log(` ┌─`);
220
+ for (const line of json.aiBriefing.resumePrompt.split('\n')) {
221
+ console.log(` │ ${line}`);
222
+ }
223
+ console.log(` └─`);
224
+ }
225
+ if (json.aiBriefing.warnings) {
226
+ console.log(`\n Heads up:`);
227
+ for (const line of json.aiBriefing.warnings.split('\n')) {
228
+ const trimmed = line.trim();
229
+ if (trimmed)
230
+ console.log(` ${trimmed}`);
231
+ }
232
+ }
233
+ }
234
+ else if (localSessions.length > 0 && json.count === 0) {
235
+ console.log(`\n Share sessions to get AI briefings with resume prompts.`);
236
+ }
237
+ }
238
+ else {
239
+ console.log(` Cloud search failed (HTTP ${res.status}).`);
240
+ }
241
+ }
242
+ catch {
243
+ console.log(` Could not reach promptarc.dev (offline?).`);
244
+ if (localSessions.length === 0) {
245
+ console.log(` No local sessions found either.`);
246
+ }
247
+ }
248
+ }
249
+ else if (localSessions.length === 0) {
250
+ console.log(`\n No local sessions found. Run \`promptarc login\` to search cloud archive.`);
251
+ }
252
+ else {
253
+ console.log(`\n Run \`promptarc login\` to also search your cloud archive.`);
254
+ }
255
+ // Session list at the bottom (least important).
256
+ if (localSessions.length > 0) {
257
+ console.log('');
258
+ for (const s of localSessions.slice(0, 6)) {
259
+ const when = fmtRelativeTime(s.mtime);
260
+ const agent = s.agent === 'claude' ? 'Claude' : s.agent === 'codex' ? 'Codex' : 'Cursor';
261
+ const cleanPrompt = s.firstPrompt
262
+ ? s.firstPrompt.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
263
+ : '';
264
+ let title = cleanPrompt || `Session ${basename(s.path, '.jsonl').slice(0, 8)}`;
265
+ if (title.length > 50) {
266
+ title = title.slice(0, 50).replace(/\s\S*$/, '') + '...';
267
+ }
268
+ console.log(` ${when.padEnd(13)} [${agent}] ${title} · ${fmtNumber(s.messageCount)} msgs`);
269
+ }
270
+ // Gentle share nudge — only when no cloud sessions exist.
271
+ if (!auth || !cloudSessionsFound) {
272
+ const latest = localSessions[0];
273
+ console.log(`\n ${localSessions.length} local session${localSessions.length === 1 ? '' : 's'} · 0 cloud sessions`);
274
+ console.log(` Share to search from any machine: promptarc share ${latest.sessionId.slice(0, 8)}`);
275
+ }
276
+ }
277
+ console.log('');
278
+ }
279
+ /**
280
+ * Scan ~/.claude/, ~/.codex/, ~/.cursor/ for sessions matching the
281
+ * current project. Returns session summaries sorted by recency.
282
+ */
283
+ function scanLocalSessions(cwd, projectName) {
284
+ const results = [];
285
+ const cwdEncoded = cwd.replace(/\//g, '-');
286
+ // Claude Code: ~/.claude/projects/<encoded-cwd>/*.jsonl
287
+ const claudeRoot = join(homedir(), '.claude', 'projects');
288
+ try {
289
+ for (const dir of readdirSync(claudeRoot)) {
290
+ if (dir === cwdEncoded || dir.endsWith(projectName)) {
291
+ const projectDir = join(claudeRoot, dir);
292
+ if (!statSync(projectDir).isDirectory())
293
+ continue;
294
+ for (const file of readdirSync(projectDir)) {
295
+ if (!file.endsWith('.jsonl'))
296
+ continue;
297
+ const fp = join(projectDir, file);
298
+ const info = quickParseSession(fp, 'claude');
299
+ if (info)
300
+ results.push(info);
301
+ }
302
+ }
303
+ }
304
+ }
305
+ catch { /* no Claude sessions */ }
306
+ // Codex: ~/.codex/sessions/**/*.jsonl (match cwd in first line)
307
+ const codexRoot = join(homedir(), '.codex', 'sessions');
308
+ try {
309
+ const codexFiles = findJsonlRecursive(codexRoot);
310
+ for (const fp of codexFiles) {
311
+ try {
312
+ const firstLine = readFileSync(fp, 'utf8').split('\n')[0] ?? '';
313
+ if (firstLine.includes(cwd) || firstLine.includes(projectName)) {
314
+ const info = quickParseSession(fp, 'codex');
315
+ if (info)
316
+ results.push(info);
317
+ }
318
+ }
319
+ catch { /* skip */ }
320
+ }
321
+ }
322
+ catch { /* no Codex sessions */ }
323
+ // Cursor: ~/.cursor/projects/<encoded>/agent-transcripts/<uuid>/<uuid>.jsonl
324
+ const cursorEncoded = cwd.replace(/\//g, '-').replace(/^-/, '');
325
+ const cursorRoot = join(homedir(), '.cursor', 'projects');
326
+ try {
327
+ for (const dir of readdirSync(cursorRoot)) {
328
+ if (dir === cursorEncoded || dir.endsWith(projectName)) {
329
+ const transcriptsDir = join(cursorRoot, dir, 'agent-transcripts');
330
+ try {
331
+ for (const sd of readdirSync(transcriptsDir)) {
332
+ const fp = join(transcriptsDir, sd, `${sd}.jsonl`);
333
+ try {
334
+ statSync(fp);
335
+ const info = quickParseSession(fp, 'cursor');
336
+ if (info)
337
+ results.push(info);
338
+ }
339
+ catch { /* skip */ }
340
+ }
341
+ }
342
+ catch { /* no transcripts */ }
343
+ }
344
+ }
345
+ }
346
+ catch { /* no Cursor sessions */ }
347
+ // Sort by most recent first.
348
+ results.sort((a, b) => b.mtime - a.mtime);
349
+ return results;
350
+ }
351
+ /**
352
+ * Quick parse a JSONL session to extract: mtime, message count,
353
+ * and the first user prompt. Reads only enough to get the data
354
+ * without loading the entire file.
355
+ */
356
+ function quickParseSession(filePath, agent) {
357
+ try {
358
+ const stat = statSync(filePath);
359
+ // For large files, read only the first 100KB to find the first prompt.
360
+ // Estimate total message count from line count of the full file.
361
+ const fullRaw = readFileSync(filePath, 'utf8');
362
+ const totalLines = fullRaw.split('\n').filter(Boolean).length;
363
+ const scanRaw = fullRaw.length > 100_000 ? fullRaw.slice(0, 100_000) : fullRaw;
364
+ const lines = scanRaw.split('\n').filter(Boolean);
365
+ if (totalLines === 0)
366
+ return null;
367
+ let messageCount = totalLines; // approximate: 1 line ≈ 1 entry
368
+ let firstPrompt = '';
369
+ for (const line of lines) {
370
+ let parsed;
371
+ try {
372
+ parsed = JSON.parse(line);
373
+ }
374
+ catch {
375
+ continue;
376
+ }
377
+ // Claude Code: {type: "user", message: "text"}
378
+ if (agent === 'claude') {
379
+ if (!firstPrompt && parsed.type === 'user') {
380
+ let text = '';
381
+ if (typeof parsed.message === 'string')
382
+ text = parsed.message;
383
+ else if (typeof parsed.message === 'object' && parsed.message) {
384
+ const content = parsed.message.content;
385
+ // content can be a string or an array of blocks.
386
+ if (typeof content === 'string')
387
+ text = content;
388
+ else if (Array.isArray(content)) {
389
+ const textBlock = content.find((b) => b.type === 'text');
390
+ if (textBlock)
391
+ text = textBlock.text;
392
+ }
393
+ }
394
+ // Skip system messages and interrupted markers.
395
+ if (text && !isSystemMessage(text))
396
+ firstPrompt = text;
397
+ }
398
+ }
399
+ // Codex: {type: "event_msg", payload: {type: "user_message", message: "..."}}
400
+ if (agent === 'codex') {
401
+ if (!firstPrompt && parsed.type === 'event_msg') {
402
+ const payload = parsed.payload;
403
+ if (payload?.type === 'user_message' && typeof payload.message === 'string') {
404
+ firstPrompt = payload.message;
405
+ }
406
+ }
407
+ }
408
+ // Cursor: {role: "user", message: {content: [{type: "text", text: "..."}]}}
409
+ if (agent === 'cursor') {
410
+ if (!firstPrompt && parsed.role === 'user' && typeof parsed.message === 'object' && parsed.message) {
411
+ const content = parsed.message.content;
412
+ if (Array.isArray(content)) {
413
+ const textBlock = content.find((b) => b.type === 'text');
414
+ if (textBlock) {
415
+ firstPrompt = textBlock.text
416
+ .replace(/<\/?user_query>/g, '')
417
+ .trim();
418
+ }
419
+ }
420
+ }
421
+ }
422
+ // Stop as soon as we find a real user prompt.
423
+ if (firstPrompt)
424
+ break;
425
+ }
426
+ if (totalLines < 2)
427
+ return null;
428
+ // Fallback title: use session ID from filename.
429
+ if (!firstPrompt) {
430
+ const name = basename(filePath, '.jsonl');
431
+ firstPrompt = `Session ${name.slice(0, 8)}... (${messageCount} messages)`;
432
+ }
433
+ // Extract session ID from filename or path.
434
+ const fileName = basename(filePath, '.jsonl');
435
+ const uuidMatch = fileName.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
436
+ const sessionId = uuidMatch?.[1] ?? fileName;
437
+ return {
438
+ path: filePath,
439
+ sessionId,
440
+ agent,
441
+ mtime: stat.mtimeMs,
442
+ messageCount,
443
+ firstPrompt,
444
+ };
445
+ }
446
+ catch {
447
+ return null;
448
+ }
449
+ }
450
+ /** Filter out system-generated messages that aren't real user prompts. */
451
+ function isSystemMessage(text) {
452
+ const t = text.trim().toLowerCase();
453
+ return (t.startsWith('[request interrupted') ||
454
+ t.startsWith('[tool use') ||
455
+ t.startsWith('<system') ||
456
+ t.startsWith('<environment') ||
457
+ t.startsWith('<permissions') ||
458
+ t.startsWith('<app-context') ||
459
+ t === '' ||
460
+ t.length < 5);
461
+ }
462
+ async function runShare(sessionArg, extraArgs) {
463
+ let auth = loadAuth();
100
464
  if (!auth) {
101
- console.error('Not signed in. Run `promptarc login` first.');
465
+ console.log(' not signed in, starting login...');
466
+ auth = await runLogin();
467
+ }
468
+ // Find the session file.
469
+ const session = sessionArg.length >= 8
470
+ ? findSessionById(sessionArg)
471
+ : null;
472
+ if (!session) {
473
+ // Try partial match — the user might pass just the first 8 chars.
474
+ const partialMatch = findSessionByPrefix(sessionArg);
475
+ if (!partialMatch) {
476
+ console.error(`Session not found: ${sessionArg}`);
477
+ console.error('Run `promptarc context` to see available sessions.');
478
+ process.exitCode = 1;
479
+ return;
480
+ }
481
+ return doShare(partialMatch, auth);
482
+ }
483
+ return doShare(session, auth);
484
+ }
485
+ async function doShare(session, auth) {
486
+ console.log(`promptarc share`);
487
+ console.log(` ▸ session: ${session.sessionId.slice(0, 8)}...`);
488
+ console.log(` ▸ building artifact locally...`);
489
+ // Extract project name from projectId.
490
+ const projectId = session.projectId ?? '';
491
+ const parts = projectId.replace(/^(codex:|cursor:)/, '').split('-').filter(Boolean);
492
+ const projectName = parts[parts.length - 1] ?? 'unknown';
493
+ // Build the SharedReplay artifact locally (reads JSONL, redacts secrets).
494
+ let replay;
495
+ try {
496
+ replay = buildReplayFromJsonl(session.path, projectName);
497
+ }
498
+ catch (err) {
499
+ console.error(` ✗ Failed to build artifact: ${String(err)}`);
500
+ process.exitCode = 1;
501
+ return;
502
+ }
503
+ console.log(` ▸ ${replay.meta.messageCount} messages, ${replay.redactionStats.secretsRedacted} secrets redacted`);
504
+ console.log(` ▸ uploading to promptarc.dev...`);
505
+ // POST directly to hosted /api/upload with the bearer token.
506
+ const siteUrl = process.env.PROMPTARC_UPLOAD_ENDPOINT ?? 'https://promptarc.dev/api/upload';
507
+ const payload = JSON.stringify({
508
+ replay,
509
+ sourceSessionId: session.sessionId,
510
+ projectKey: projectId,
511
+ agentSource: projectId.startsWith('codex:') ? 'codex' : projectId.startsWith('cursor:') ? 'cursor' : 'claude-code',
512
+ visibility: 'private',
513
+ autoSync: false,
514
+ _h: '',
515
+ });
516
+ if (payload.length > 5 * 1024 * 1024) {
517
+ console.error(` ✗ Session too large (${(payload.length / 1024 / 1024).toFixed(1)} MB). Max 5 MB.`);
518
+ process.exitCode = 1;
519
+ return;
520
+ }
521
+ try {
522
+ const res = await fetch(siteUrl, {
523
+ method: 'POST',
524
+ headers: {
525
+ 'content-type': 'application/json',
526
+ authorization: `Bearer ${auth.token}`,
527
+ },
528
+ body: payload,
529
+ });
530
+ if (!res.ok) {
531
+ const text = await res.text();
532
+ console.error(` ✗ Upload failed: HTTP ${res.status}`);
533
+ console.error(` ${text.slice(0, 200)}`);
534
+ process.exitCode = 1;
535
+ return;
536
+ }
537
+ const json = (await res.json());
538
+ if (json.status === 'unchanged') {
539
+ console.log(` ✓ Already shared (unchanged).`);
540
+ }
541
+ else {
542
+ console.log(` ✓ Shared.`);
543
+ }
544
+ if (json.url) {
545
+ console.log(` ${json.url}`);
546
+ }
547
+ }
548
+ catch (err) {
549
+ console.error(` ✗ Upload failed: ${String(err)}`);
102
550
  process.exitCode = 1;
103
551
  return;
104
552
  }
553
+ console.log('');
554
+ }
555
+ function findSessionByPrefix(prefix) {
556
+ const claudeRoot = join(homedir(), '.claude', 'projects');
557
+ try {
558
+ for (const dir of readdirSync(claudeRoot)) {
559
+ const projectDir = join(claudeRoot, dir);
560
+ if (!statSync(projectDir).isDirectory())
561
+ continue;
562
+ for (const file of readdirSync(projectDir)) {
563
+ if (file.startsWith(prefix) && file.endsWith('.jsonl')) {
564
+ return {
565
+ path: join(projectDir, file),
566
+ sessionId: basename(file, '.jsonl'),
567
+ projectId: dir,
568
+ };
569
+ }
570
+ }
571
+ }
572
+ }
573
+ catch { /* no Claude projects */ }
574
+ // Cursor.
575
+ const cursorRoot = join(homedir(), '.cursor', 'projects');
576
+ try {
577
+ for (const dir of readdirSync(cursorRoot)) {
578
+ const transcriptsDir = join(cursorRoot, dir, 'agent-transcripts');
579
+ try {
580
+ for (const sd of readdirSync(transcriptsDir)) {
581
+ if (sd.startsWith(prefix)) {
582
+ const fp = join(transcriptsDir, sd, `${sd}.jsonl`);
583
+ try {
584
+ statSync(fp);
585
+ return { path: fp, sessionId: sd, projectId: `cursor:${dir}` };
586
+ }
587
+ catch { /* skip */ }
588
+ }
589
+ }
590
+ }
591
+ catch { /* no transcripts */ }
592
+ }
593
+ }
594
+ catch { /* no Cursor */ }
595
+ return null;
596
+ }
597
+ function fmtRelativeTime(ms) {
598
+ const diff = Date.now() - ms;
599
+ const mins = Math.floor(diff / 60_000);
600
+ if (mins < 1)
601
+ return 'just now';
602
+ if (mins < 60)
603
+ return `${mins}m ago`;
604
+ const hours = Math.floor(mins / 60);
605
+ if (hours < 24)
606
+ return `${hours}h ago`;
607
+ const days = Math.floor(hours / 24);
608
+ if (days === 1)
609
+ return 'yesterday';
610
+ if (days < 7)
611
+ return `${days}d ago`;
612
+ const weeks = Math.floor(days / 7);
613
+ if (weeks < 5)
614
+ return `${weeks}w ago`;
615
+ const months = Math.floor(days / 30);
616
+ return `${months}mo ago`;
617
+ }
618
+ function fmtNumber(n) {
619
+ return n.toLocaleString('en-US');
620
+ }
621
+ async function runPR(args) {
622
+ let auth = loadAuth();
623
+ if (!auth) {
624
+ console.log(' ▸ not signed in, starting login...');
625
+ auth = await runLogin();
626
+ }
105
627
  const shouldShare = args.includes('--share');
106
628
  const sessionIdArg = args.find((a) => !a.startsWith('--'));
107
629
  const copyFlag = args.includes('--copy');
@@ -206,27 +728,25 @@ async function runPR(args) {
206
728
  console.log('');
207
729
  }
208
730
  function findLatestSession(cwd) {
209
- const fs = require('node:fs');
210
- const path = require('node:path');
211
731
  const candidates = [];
212
732
  const cwdParts = cwd.split('/').filter(Boolean);
213
733
  const lastPart = cwdParts[cwdParts.length - 1] ?? '';
214
734
  const encoded = cwd.replace(/\//g, '-');
215
735
  // 1. Claude Code: ~/.claude/projects/<encoded-cwd>/*.jsonl
216
- const claudeRoot = path.join(homedir(), '.claude', 'projects');
736
+ const claudeRoot = join(homedir(), '.claude', 'projects');
217
737
  try {
218
- const dirs = fs.readdirSync(claudeRoot);
738
+ const dirs = readdirSync(claudeRoot);
219
739
  for (const dir of dirs) {
220
740
  if (dir === encoded || dir.endsWith(lastPart)) {
221
- const projectDir = path.join(claudeRoot, dir);
222
- if (!fs.statSync(projectDir).isDirectory())
741
+ const projectDir = join(claudeRoot, dir);
742
+ if (!statSync(projectDir).isDirectory())
223
743
  continue;
224
- const files = fs.readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
744
+ const files = readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
225
745
  for (const f of files) {
226
- const fp = path.join(projectDir, f);
746
+ const fp = join(projectDir, f);
227
747
  candidates.push({
228
748
  path: fp,
229
- sessionId: path.basename(f, '.jsonl'),
749
+ sessionId: basename(f, '.jsonl'),
230
750
  projectId: dir,
231
751
  });
232
752
  }
@@ -235,15 +755,15 @@ function findLatestSession(cwd) {
235
755
  }
236
756
  catch { /* no Claude sessions */ }
237
757
  // 2. Codex: ~/.codex/sessions/**/*.jsonl (scan all, match cwd from session_meta)
238
- const codexRoot = path.join(homedir(), '.codex', 'sessions');
758
+ const codexRoot = join(homedir(), '.codex', 'sessions');
239
759
  try {
240
- const codexFiles = findJsonlRecursive(codexRoot, fs, path);
760
+ const codexFiles = findJsonlRecursive(codexRoot);
241
761
  for (const fp of codexFiles) {
242
762
  // Quick check: read first line for cwd match.
243
763
  try {
244
- const firstLine = fs.readFileSync(fp, 'utf8').split('\n')[0] ?? '';
764
+ const firstLine = readFileSync(fp, 'utf8').split('\n')[0] ?? '';
245
765
  if (firstLine.includes(cwd) || firstLine.includes(lastPart)) {
246
- const name = path.basename(fp, '.jsonl');
766
+ const name = basename(fp, '.jsonl');
247
767
  const uuidMatch = name.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
248
768
  candidates.push({
249
769
  path: fp,
@@ -258,18 +778,18 @@ function findLatestSession(cwd) {
258
778
  catch { /* no Codex sessions */ }
259
779
  // 3. Cursor: ~/.cursor/projects/<encoded>/agent-transcripts/<uuid>/<uuid>.jsonl
260
780
  const cursorEncoded = cwd.replace(/\//g, '-').replace(/^-/, '');
261
- const cursorRoot = path.join(homedir(), '.cursor', 'projects');
781
+ const cursorRoot = join(homedir(), '.cursor', 'projects');
262
782
  try {
263
- const dirs = fs.readdirSync(cursorRoot);
783
+ const dirs = readdirSync(cursorRoot);
264
784
  for (const dir of dirs) {
265
785
  if (dir === cursorEncoded || dir.endsWith(lastPart)) {
266
- const transcriptsDir = path.join(cursorRoot, dir, 'agent-transcripts');
786
+ const transcriptsDir = join(cursorRoot, dir, 'agent-transcripts');
267
787
  try {
268
- const sessionDirs = fs.readdirSync(transcriptsDir);
788
+ const sessionDirs = readdirSync(transcriptsDir);
269
789
  for (const sd of sessionDirs) {
270
- const fp = path.join(transcriptsDir, sd, `${sd}.jsonl`);
790
+ const fp = join(transcriptsDir, sd, `${sd}.jsonl`);
271
791
  try {
272
- fs.statSync(fp);
792
+ statSync(fp);
273
793
  candidates.push({
274
794
  path: fp,
275
795
  sessionId: sd,
@@ -289,7 +809,7 @@ function findLatestSession(cwd) {
289
809
  // Return the most recently modified file.
290
810
  candidates.sort((a, b) => {
291
811
  try {
292
- return fs.statSync(b.path).mtimeMs - fs.statSync(a.path).mtimeMs;
812
+ return statSync(b.path).mtimeMs - statSync(a.path).mtimeMs;
293
813
  }
294
814
  catch {
295
815
  return 0;
@@ -297,14 +817,14 @@ function findLatestSession(cwd) {
297
817
  });
298
818
  return candidates[0];
299
819
  }
300
- function findJsonlRecursive(dir, fs, path) {
820
+ function findJsonlRecursive(dir) {
301
821
  const results = [];
302
822
  try {
303
- const entries = fs.readdirSync(dir, { withFileTypes: true });
823
+ const entries = readdirSync(dir, { withFileTypes: true });
304
824
  for (const entry of entries) {
305
- const full = path.join(dir, entry.name);
825
+ const full = join(dir, entry.name);
306
826
  if (entry.isDirectory())
307
- results.push(...findJsonlRecursive(full, fs, path));
827
+ results.push(...findJsonlRecursive(full));
308
828
  else if (entry.name.endsWith('.jsonl'))
309
829
  results.push(full);
310
830
  }
@@ -313,15 +833,13 @@ function findJsonlRecursive(dir, fs, path) {
313
833
  return results;
314
834
  }
315
835
  function findSessionById(id) {
316
- const fs = require('node:fs');
317
- const path = require('node:path');
318
836
  // Search Claude Code.
319
- const claudeRoot = path.join(homedir(), '.claude', 'projects');
837
+ const claudeRoot = join(homedir(), '.claude', 'projects');
320
838
  try {
321
- for (const dir of fs.readdirSync(claudeRoot)) {
322
- const fp = path.join(claudeRoot, dir, `${id}.jsonl`);
839
+ for (const dir of readdirSync(claudeRoot)) {
840
+ const fp = join(claudeRoot, dir, `${id}.jsonl`);
323
841
  try {
324
- fs.statSync(fp);
842
+ statSync(fp);
325
843
  return { path: fp, sessionId: id, projectId: dir };
326
844
  }
327
845
  catch { /* skip */ }
@@ -329,12 +847,12 @@ function findSessionById(id) {
329
847
  }
330
848
  catch { /* no Claude */ }
331
849
  // Search Cursor.
332
- const cursorRoot = path.join(homedir(), '.cursor', 'projects');
850
+ const cursorRoot = join(homedir(), '.cursor', 'projects');
333
851
  try {
334
- for (const dir of fs.readdirSync(cursorRoot)) {
335
- const fp = path.join(cursorRoot, dir, 'agent-transcripts', id, `${id}.jsonl`);
852
+ for (const dir of readdirSync(cursorRoot)) {
853
+ const fp = join(cursorRoot, dir, 'agent-transcripts', id, `${id}.jsonl`);
336
854
  try {
337
- fs.statSync(fp);
855
+ statSync(fp);
338
856
  return { path: fp, sessionId: id, projectId: `cursor:${dir}` };
339
857
  }
340
858
  catch { /* skip */ }
@@ -342,8 +860,8 @@ function findSessionById(id) {
342
860
  }
343
861
  catch { /* no Cursor */ }
344
862
  // Search Codex (scan all, match by UUID in filename).
345
- const codexRoot = path.join(homedir(), '.codex', 'sessions');
346
- const codexFiles = findJsonlRecursive(codexRoot, fs, path);
863
+ const codexRoot = join(homedir(), '.codex', 'sessions');
864
+ const codexFiles = findJsonlRecursive(codexRoot);
347
865
  for (const fp of codexFiles) {
348
866
  if (fp.includes(id)) {
349
867
  return { path: fp, sessionId: id, projectId: null };
@@ -357,7 +875,6 @@ function findSessionById(id) {
357
875
  * names + file paths, and error outputs. Capped at 4K chars.
358
876
  */
359
877
  function extractLocalSummaryInput(filePath) {
360
- const { readFileSync } = require('node:fs');
361
878
  const raw = readFileSync(filePath, 'utf8');
362
879
  const lines = raw.split('\n').filter(Boolean);
363
880
  const parts = [];
@@ -456,11 +973,10 @@ function extractLocalSummaryInput(filePath) {
456
973
  return parts.join('\n').slice(0, 4000);
457
974
  }
458
975
  async function runPull(slug) {
459
- const auth = loadAuth();
976
+ let auth = loadAuth();
460
977
  if (!auth) {
461
- console.error('Not signed in. Run `promptarc login` first.');
462
- process.exitCode = 1;
463
- return;
978
+ console.log(' not signed in, starting login...');
979
+ auth = await runLogin();
464
980
  }
465
981
  console.log(`promptarc pull ${slug}`);
466
982
  console.log(' ▸ downloading session...');