offwatch 0.5.8 → 0.5.10

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 (94) hide show
  1. package/bin/offwatch.js +7 -6
  2. package/package.json +4 -3
  3. package/src/__tests__/agent-jwt-env.test.ts +79 -0
  4. package/src/__tests__/allowed-hostname.test.ts +80 -0
  5. package/src/__tests__/auth-command-registration.test.ts +16 -0
  6. package/src/__tests__/board-auth.test.ts +53 -0
  7. package/src/__tests__/common.test.ts +98 -0
  8. package/src/__tests__/company-delete.test.ts +95 -0
  9. package/src/__tests__/company-import-export-e2e.test.ts +502 -0
  10. package/src/__tests__/company-import-url.test.ts +74 -0
  11. package/src/__tests__/company-import-zip.test.ts +44 -0
  12. package/src/__tests__/company.test.ts +599 -0
  13. package/src/__tests__/context.test.ts +70 -0
  14. package/src/__tests__/data-dir.test.ts +79 -0
  15. package/src/__tests__/doctor.test.ts +102 -0
  16. package/src/__tests__/feedback.test.ts +177 -0
  17. package/src/__tests__/helpers/embedded-postgres.ts +6 -0
  18. package/src/__tests__/helpers/zip.ts +87 -0
  19. package/src/__tests__/home-paths.test.ts +44 -0
  20. package/src/__tests__/http.test.ts +106 -0
  21. package/src/__tests__/network-bind.test.ts +62 -0
  22. package/src/__tests__/onboard.test.ts +166 -0
  23. package/src/__tests__/routines.test.ts +249 -0
  24. package/src/__tests__/telemetry.test.ts +117 -0
  25. package/src/__tests__/worktree-merge-history.test.ts +492 -0
  26. package/src/__tests__/worktree.test.ts +982 -0
  27. package/src/adapters/http/format-event.ts +4 -0
  28. package/src/adapters/http/index.ts +7 -0
  29. package/src/adapters/index.ts +2 -0
  30. package/src/adapters/process/format-event.ts +4 -0
  31. package/src/adapters/process/index.ts +7 -0
  32. package/src/adapters/registry.ts +63 -0
  33. package/src/checks/agent-jwt-secret-check.ts +40 -0
  34. package/src/checks/config-check.ts +33 -0
  35. package/src/checks/database-check.ts +59 -0
  36. package/src/checks/deployment-auth-check.ts +88 -0
  37. package/src/checks/index.ts +18 -0
  38. package/src/checks/llm-check.ts +82 -0
  39. package/src/checks/log-check.ts +30 -0
  40. package/src/checks/path-resolver.ts +1 -0
  41. package/src/checks/port-check.ts +24 -0
  42. package/src/checks/secrets-check.ts +146 -0
  43. package/src/checks/storage-check.ts +51 -0
  44. package/src/client/board-auth.ts +282 -0
  45. package/src/client/command-label.ts +4 -0
  46. package/src/client/context.ts +175 -0
  47. package/src/client/http.ts +255 -0
  48. package/src/commands/allowed-hostname.ts +40 -0
  49. package/src/commands/auth-bootstrap-ceo.ts +138 -0
  50. package/src/commands/client/activity.ts +71 -0
  51. package/src/commands/client/agent.ts +315 -0
  52. package/src/commands/client/approval.ts +259 -0
  53. package/src/commands/client/auth.ts +113 -0
  54. package/src/commands/client/common.ts +221 -0
  55. package/src/commands/client/company.ts +1578 -0
  56. package/src/commands/client/context.ts +125 -0
  57. package/src/commands/client/dashboard.ts +34 -0
  58. package/src/commands/client/feedback.ts +645 -0
  59. package/src/commands/client/issue.ts +411 -0
  60. package/src/commands/client/plugin.ts +374 -0
  61. package/src/commands/client/zip.ts +129 -0
  62. package/src/commands/configure.ts +201 -0
  63. package/src/commands/db-backup.ts +102 -0
  64. package/src/commands/doctor.ts +203 -0
  65. package/src/commands/env.ts +411 -0
  66. package/src/commands/heartbeat-run.ts +344 -0
  67. package/src/commands/onboard.ts +692 -0
  68. package/src/commands/routines.ts +352 -0
  69. package/src/commands/run.ts +216 -0
  70. package/src/commands/worktree-lib.ts +279 -0
  71. package/src/commands/worktree-merge-history-lib.ts +764 -0
  72. package/src/commands/worktree.ts +2876 -0
  73. package/src/config/data-dir.ts +48 -0
  74. package/src/config/env.ts +125 -0
  75. package/src/config/home.ts +80 -0
  76. package/src/config/hostnames.ts +26 -0
  77. package/src/config/schema.ts +30 -0
  78. package/src/config/secrets-key.ts +48 -0
  79. package/src/config/server-bind.ts +183 -0
  80. package/src/config/store.ts +120 -0
  81. package/src/index.ts +182 -0
  82. package/src/prompts/database.ts +157 -0
  83. package/src/prompts/llm.ts +43 -0
  84. package/src/prompts/logging.ts +37 -0
  85. package/src/prompts/secrets.ts +99 -0
  86. package/src/prompts/server.ts +221 -0
  87. package/src/prompts/storage.ts +146 -0
  88. package/src/telemetry.ts +49 -0
  89. package/src/utils/banner.ts +24 -0
  90. package/src/utils/net.ts +18 -0
  91. package/src/utils/path-resolver.ts +25 -0
  92. package/src/version.ts +10 -0
  93. package/lib/downloader.js +0 -112
  94. package/postinstall.js +0 -23
@@ -0,0 +1,599 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { CompanyPortabilityPreviewResult } from "@paperclipai/shared";
3
+ import {
4
+ buildCompanyDashboardUrl,
5
+ buildDefaultImportAdapterOverrides,
6
+ buildDefaultImportSelectionState,
7
+ buildImportSelectionCatalog,
8
+ buildSelectedFilesFromImportSelection,
9
+ renderCompanyImportPreview,
10
+ renderCompanyImportResult,
11
+ resolveCompanyImportApplyConfirmationMode,
12
+ resolveCompanyImportApiPath,
13
+ } from "../commands/client/company.js";
14
+
15
+ describe("resolveCompanyImportApiPath", () => {
16
+ it("uses company-scoped preview route for existing-company dry runs", () => {
17
+ expect(
18
+ resolveCompanyImportApiPath({
19
+ dryRun: true,
20
+ targetMode: "existing_company",
21
+ companyId: "company-123",
22
+ }),
23
+ ).toBe("/api/companies/company-123/imports/preview");
24
+ });
25
+
26
+ it("uses company-scoped apply route for existing-company imports", () => {
27
+ expect(
28
+ resolveCompanyImportApiPath({
29
+ dryRun: false,
30
+ targetMode: "existing_company",
31
+ companyId: "company-123",
32
+ }),
33
+ ).toBe("/api/companies/company-123/imports/apply");
34
+ });
35
+
36
+ it("keeps global routes for new-company imports", () => {
37
+ expect(
38
+ resolveCompanyImportApiPath({
39
+ dryRun: true,
40
+ targetMode: "new_company",
41
+ }),
42
+ ).toBe("/api/companies/import/preview");
43
+
44
+ expect(
45
+ resolveCompanyImportApiPath({
46
+ dryRun: false,
47
+ targetMode: "new_company",
48
+ }),
49
+ ).toBe("/api/companies/import");
50
+ });
51
+
52
+ it("throws when an existing-company import is missing a company id", () => {
53
+ expect(() =>
54
+ resolveCompanyImportApiPath({
55
+ dryRun: true,
56
+ targetMode: "existing_company",
57
+ companyId: " ",
58
+ })
59
+ ).toThrow(/require a companyId/i);
60
+ });
61
+ });
62
+
63
+ describe("resolveCompanyImportApplyConfirmationMode", () => {
64
+ it("skips confirmation when --yes is set", () => {
65
+ expect(
66
+ resolveCompanyImportApplyConfirmationMode({
67
+ yes: true,
68
+ interactive: false,
69
+ json: false,
70
+ }),
71
+ ).toBe("skip");
72
+ });
73
+
74
+ it("prompts in interactive text mode when --yes is not set", () => {
75
+ expect(
76
+ resolveCompanyImportApplyConfirmationMode({
77
+ yes: false,
78
+ interactive: true,
79
+ json: false,
80
+ }),
81
+ ).toBe("prompt");
82
+ });
83
+
84
+ it("requires --yes for non-interactive apply", () => {
85
+ expect(() =>
86
+ resolveCompanyImportApplyConfirmationMode({
87
+ yes: false,
88
+ interactive: false,
89
+ json: false,
90
+ })
91
+ ).toThrow(/non-interactive terminal requires --yes/i);
92
+ });
93
+
94
+ it("requires --yes for json apply", () => {
95
+ expect(() =>
96
+ resolveCompanyImportApplyConfirmationMode({
97
+ yes: false,
98
+ interactive: false,
99
+ json: true,
100
+ })
101
+ ).toThrow(/with --json requires --yes/i);
102
+ });
103
+ });
104
+
105
+ describe("buildCompanyDashboardUrl", () => {
106
+ it("preserves the configured base path when building a dashboard URL", () => {
107
+ expect(buildCompanyDashboardUrl("https://paperclip.example/app/", "PAP")).toBe(
108
+ "https://paperclip.example/app/PAP/dashboard",
109
+ );
110
+ });
111
+ });
112
+
113
+ describe("renderCompanyImportPreview", () => {
114
+ it("summarizes the preview with counts, selection info, and truncated examples", () => {
115
+ const preview: CompanyPortabilityPreviewResult = {
116
+ include: {
117
+ company: true,
118
+ agents: true,
119
+ projects: true,
120
+ issues: true,
121
+ skills: true,
122
+ },
123
+ targetCompanyId: "company-123",
124
+ targetCompanyName: "Imported Co",
125
+ collisionStrategy: "rename",
126
+ selectedAgentSlugs: ["ceo", "cto", "eng-1", "eng-2", "eng-3", "eng-4", "eng-5"],
127
+ plan: {
128
+ companyAction: "update",
129
+ agentPlans: [
130
+ { slug: "ceo", action: "create", plannedName: "CEO", existingAgentId: null, reason: null },
131
+ { slug: "cto", action: "update", plannedName: "CTO", existingAgentId: "agent-2", reason: "replace strategy" },
132
+ { slug: "eng-1", action: "skip", plannedName: "Engineer 1", existingAgentId: "agent-3", reason: "skip strategy" },
133
+ { slug: "eng-2", action: "create", plannedName: "Engineer 2", existingAgentId: null, reason: null },
134
+ { slug: "eng-3", action: "create", plannedName: "Engineer 3", existingAgentId: null, reason: null },
135
+ { slug: "eng-4", action: "create", plannedName: "Engineer 4", existingAgentId: null, reason: null },
136
+ { slug: "eng-5", action: "create", plannedName: "Engineer 5", existingAgentId: null, reason: null },
137
+ ],
138
+ projectPlans: [
139
+ { slug: "alpha", action: "create", plannedName: "Alpha", existingProjectId: null, reason: null },
140
+ ],
141
+ issuePlans: [
142
+ { slug: "kickoff", action: "create", plannedTitle: "Kickoff", reason: null },
143
+ ],
144
+ },
145
+ manifest: {
146
+ schemaVersion: 1,
147
+ generatedAt: "2026-03-23T17:00:00.000Z",
148
+ source: {
149
+ companyId: "company-src",
150
+ companyName: "Source Co",
151
+ },
152
+ includes: {
153
+ company: true,
154
+ agents: true,
155
+ projects: true,
156
+ issues: true,
157
+ skills: true,
158
+ },
159
+ company: {
160
+ path: "COMPANY.md",
161
+ name: "Source Co",
162
+ description: null,
163
+ brandColor: null,
164
+ logoPath: null,
165
+ requireBoardApprovalForNewAgents: false,
166
+ feedbackDataSharingEnabled: false,
167
+ feedbackDataSharingConsentAt: null,
168
+ feedbackDataSharingConsentByUserId: null,
169
+ feedbackDataSharingTermsVersion: null,
170
+ },
171
+ sidebar: {
172
+ agents: ["ceo"],
173
+ projects: ["alpha"],
174
+ },
175
+ agents: [
176
+ {
177
+ slug: "ceo",
178
+ name: "CEO",
179
+ path: "agents/ceo/AGENT.md",
180
+ skills: [],
181
+ role: "ceo",
182
+ title: null,
183
+ icon: null,
184
+ capabilities: null,
185
+ reportsToSlug: null,
186
+ adapterType: "codex_local",
187
+ adapterConfig: {},
188
+ runtimeConfig: {},
189
+ permissions: {},
190
+ budgetMonthlyCents: 0,
191
+ metadata: null,
192
+ },
193
+ ],
194
+ skills: [
195
+ {
196
+ key: "skill-a",
197
+ slug: "skill-a",
198
+ name: "Skill A",
199
+ path: "skills/skill-a/SKILL.md",
200
+ description: null,
201
+ sourceType: "inline",
202
+ sourceLocator: null,
203
+ sourceRef: null,
204
+ trustLevel: null,
205
+ compatibility: null,
206
+ metadata: null,
207
+ fileInventory: [],
208
+ },
209
+ ],
210
+ projects: [
211
+ {
212
+ slug: "alpha",
213
+ name: "Alpha",
214
+ path: "projects/alpha/PROJECT.md",
215
+ description: null,
216
+ ownerAgentSlug: null,
217
+ leadAgentSlug: null,
218
+ targetDate: null,
219
+ color: null,
220
+ status: null,
221
+ executionWorkspacePolicy: null,
222
+ workspaces: [],
223
+ env: null,
224
+ metadata: null,
225
+ },
226
+ ],
227
+ issues: [
228
+ {
229
+ slug: "kickoff",
230
+ identifier: null,
231
+ title: "Kickoff",
232
+ path: "projects/alpha/issues/kickoff/TASK.md",
233
+ projectSlug: "alpha",
234
+ projectWorkspaceKey: null,
235
+ assigneeAgentSlug: "ceo",
236
+ description: null,
237
+ recurring: false,
238
+ routine: null,
239
+ legacyRecurrence: null,
240
+ status: null,
241
+ priority: null,
242
+ labelIds: [],
243
+ billingCode: null,
244
+ executionWorkspaceSettings: null,
245
+ assigneeAdapterOverrides: null,
246
+ metadata: null,
247
+ },
248
+ ],
249
+ envInputs: [
250
+ {
251
+ key: "OPENAI_API_KEY",
252
+ description: null,
253
+ agentSlug: "ceo",
254
+ projectSlug: null,
255
+ kind: "secret",
256
+ requirement: "required",
257
+ defaultValue: null,
258
+ portability: "portable",
259
+ },
260
+ ],
261
+ },
262
+ files: {
263
+ "COMPANY.md": "# Source Co",
264
+ },
265
+ envInputs: [
266
+ {
267
+ key: "OPENAI_API_KEY",
268
+ description: null,
269
+ agentSlug: "ceo",
270
+ projectSlug: null,
271
+ kind: "secret",
272
+ requirement: "required",
273
+ defaultValue: null,
274
+ portability: "portable",
275
+ },
276
+ ],
277
+ warnings: ["One warning"],
278
+ errors: ["One error"],
279
+ };
280
+
281
+ const rendered = renderCompanyImportPreview(preview, {
282
+ sourceLabel: "GitHub: https://github.com/paperclipai/companies/demo",
283
+ targetLabel: "Imported Co (company-123)",
284
+ infoMessages: ["Using claude-local adapter"],
285
+ });
286
+
287
+ expect(rendered).toContain("Include");
288
+ expect(rendered).toContain("company, projects, tasks, agents, skills");
289
+ expect(rendered).toContain("7 agents total");
290
+ expect(rendered).toContain("1 project total");
291
+ expect(rendered).toContain("1 task total");
292
+ expect(rendered).toContain("skills: 1 skill packaged");
293
+ expect(rendered).toContain("+1 more");
294
+ expect(rendered).toContain("Using claude-local adapter");
295
+ expect(rendered).toContain("Warnings");
296
+ expect(rendered).toContain("Errors");
297
+ });
298
+ });
299
+
300
+ describe("renderCompanyImportResult", () => {
301
+ it("summarizes import results with created, updated, and skipped counts", () => {
302
+ const rendered = renderCompanyImportResult(
303
+ {
304
+ company: {
305
+ id: "company-123",
306
+ name: "Imported Co",
307
+ action: "updated",
308
+ },
309
+ agents: [
310
+ { slug: "ceo", id: "agent-1", action: "created", name: "CEO", reason: null },
311
+ { slug: "cto", id: "agent-2", action: "updated", name: "CTO", reason: "replace strategy" },
312
+ { slug: "ops", id: null, action: "skipped", name: "Ops", reason: "skip strategy" },
313
+ ],
314
+ projects: [
315
+ { slug: "app", id: "project-1", action: "created", name: "App", reason: null },
316
+ { slug: "ops", id: "project-2", action: "updated", name: "Operations", reason: "replace strategy" },
317
+ { slug: "archive", id: null, action: "skipped", name: "Archive", reason: "skip strategy" },
318
+ ],
319
+ envInputs: [],
320
+ warnings: ["Review API keys"],
321
+ },
322
+ {
323
+ targetLabel: "Imported Co (company-123)",
324
+ companyUrl: "https://paperclip.example/PAP/dashboard",
325
+ infoMessages: ["Using claude-local adapter"],
326
+ },
327
+ );
328
+
329
+ expect(rendered).toContain("Company");
330
+ expect(rendered).toContain("https://paperclip.example/PAP/dashboard");
331
+ expect(rendered).toContain("3 agents total (1 created, 1 updated, 1 skipped)");
332
+ expect(rendered).toContain("3 projects total (1 created, 1 updated, 1 skipped)");
333
+ expect(rendered).toContain("Agent results");
334
+ expect(rendered).toContain("Project results");
335
+ expect(rendered).toContain("Using claude-local adapter");
336
+ expect(rendered).toContain("Review API keys");
337
+ });
338
+ });
339
+
340
+ describe("import selection catalog", () => {
341
+ it("defaults to everything and keeps project selection separate from task selection", () => {
342
+ const preview: CompanyPortabilityPreviewResult = {
343
+ include: {
344
+ company: true,
345
+ agents: true,
346
+ projects: true,
347
+ issues: true,
348
+ skills: true,
349
+ },
350
+ targetCompanyId: "company-123",
351
+ targetCompanyName: "Imported Co",
352
+ collisionStrategy: "rename",
353
+ selectedAgentSlugs: ["ceo"],
354
+ plan: {
355
+ companyAction: "create",
356
+ agentPlans: [],
357
+ projectPlans: [],
358
+ issuePlans: [],
359
+ },
360
+ manifest: {
361
+ schemaVersion: 1,
362
+ generatedAt: "2026-03-23T18:00:00.000Z",
363
+ source: {
364
+ companyId: "company-src",
365
+ companyName: "Source Co",
366
+ },
367
+ includes: {
368
+ company: true,
369
+ agents: true,
370
+ projects: true,
371
+ issues: true,
372
+ skills: true,
373
+ },
374
+ company: {
375
+ path: "COMPANY.md",
376
+ name: "Source Co",
377
+ description: null,
378
+ brandColor: null,
379
+ logoPath: "images/company-logo.png",
380
+ requireBoardApprovalForNewAgents: false,
381
+ feedbackDataSharingEnabled: false,
382
+ feedbackDataSharingConsentAt: null,
383
+ feedbackDataSharingConsentByUserId: null,
384
+ feedbackDataSharingTermsVersion: null,
385
+ },
386
+ sidebar: {
387
+ agents: ["ceo"],
388
+ projects: ["alpha"],
389
+ },
390
+ agents: [
391
+ {
392
+ slug: "ceo",
393
+ name: "CEO",
394
+ path: "agents/ceo/AGENT.md",
395
+ skills: [],
396
+ role: "ceo",
397
+ title: null,
398
+ icon: null,
399
+ capabilities: null,
400
+ reportsToSlug: null,
401
+ adapterType: "codex_local",
402
+ adapterConfig: {},
403
+ runtimeConfig: {},
404
+ permissions: {},
405
+ budgetMonthlyCents: 0,
406
+ metadata: null,
407
+ },
408
+ ],
409
+ skills: [
410
+ {
411
+ key: "skill-a",
412
+ slug: "skill-a",
413
+ name: "Skill A",
414
+ path: "skills/skill-a/SKILL.md",
415
+ description: null,
416
+ sourceType: "inline",
417
+ sourceLocator: null,
418
+ sourceRef: null,
419
+ trustLevel: null,
420
+ compatibility: null,
421
+ metadata: null,
422
+ fileInventory: [{ path: "skills/skill-a/helper.md", kind: "doc" }],
423
+ },
424
+ ],
425
+ projects: [
426
+ {
427
+ slug: "alpha",
428
+ name: "Alpha",
429
+ path: "projects/alpha/PROJECT.md",
430
+ description: null,
431
+ ownerAgentSlug: null,
432
+ leadAgentSlug: null,
433
+ targetDate: null,
434
+ color: null,
435
+ status: null,
436
+ executionWorkspacePolicy: null,
437
+ workspaces: [],
438
+ env: null,
439
+ metadata: null,
440
+ },
441
+ ],
442
+ issues: [
443
+ {
444
+ slug: "kickoff",
445
+ identifier: null,
446
+ title: "Kickoff",
447
+ path: "projects/alpha/issues/kickoff/TASK.md",
448
+ projectSlug: "alpha",
449
+ projectWorkspaceKey: null,
450
+ assigneeAgentSlug: "ceo",
451
+ description: null,
452
+ recurring: false,
453
+ routine: null,
454
+ legacyRecurrence: null,
455
+ status: null,
456
+ priority: null,
457
+ labelIds: [],
458
+ billingCode: null,
459
+ executionWorkspaceSettings: null,
460
+ assigneeAdapterOverrides: null,
461
+ metadata: null,
462
+ },
463
+ ],
464
+ envInputs: [],
465
+ },
466
+ files: {
467
+ "COMPANY.md": "# Source Co",
468
+ "README.md": "# Readme",
469
+ ".paperclip.yaml": "schema: paperclip/v1\n",
470
+ "images/company-logo.png": {
471
+ encoding: "base64",
472
+ data: "",
473
+ contentType: "image/png",
474
+ },
475
+ "projects/alpha/PROJECT.md": "# Alpha",
476
+ "projects/alpha/notes.md": "project notes",
477
+ "projects/alpha/issues/kickoff/TASK.md": "# Kickoff",
478
+ "projects/alpha/issues/kickoff/details.md": "task details",
479
+ "agents/ceo/AGENT.md": "# CEO",
480
+ "agents/ceo/prompt.md": "prompt",
481
+ "skills/skill-a/SKILL.md": "# Skill A",
482
+ "skills/skill-a/helper.md": "helper",
483
+ },
484
+ envInputs: [],
485
+ warnings: [],
486
+ errors: [],
487
+ };
488
+
489
+ const catalog = buildImportSelectionCatalog(preview);
490
+ const state = buildDefaultImportSelectionState(catalog);
491
+
492
+ expect(state.company).toBe(true);
493
+ expect(state.projects.has("alpha")).toBe(true);
494
+ expect(state.issues.has("kickoff")).toBe(true);
495
+ expect(state.agents.has("ceo")).toBe(true);
496
+ expect(state.skills.has("skill-a")).toBe(true);
497
+
498
+ state.company = false;
499
+ state.issues.clear();
500
+ state.agents.clear();
501
+ state.skills.clear();
502
+
503
+ const selectedFiles = buildSelectedFilesFromImportSelection(catalog, state);
504
+
505
+ expect(selectedFiles).toContain(".paperclip.yaml");
506
+ expect(selectedFiles).toContain("projects/alpha/PROJECT.md");
507
+ expect(selectedFiles).toContain("projects/alpha/notes.md");
508
+ expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/TASK.md");
509
+ expect(selectedFiles).not.toContain("projects/alpha/issues/kickoff/details.md");
510
+ });
511
+ });
512
+
513
+ describe("default adapter overrides", () => {
514
+ it("maps process-only imported agents to claude_local", () => {
515
+ const preview: CompanyPortabilityPreviewResult = {
516
+ include: {
517
+ company: false,
518
+ agents: true,
519
+ projects: false,
520
+ issues: false,
521
+ skills: false,
522
+ },
523
+ targetCompanyId: null,
524
+ targetCompanyName: null,
525
+ collisionStrategy: "rename",
526
+ selectedAgentSlugs: ["legacy-agent", "explicit-agent"],
527
+ plan: {
528
+ companyAction: "none",
529
+ agentPlans: [],
530
+ projectPlans: [],
531
+ issuePlans: [],
532
+ },
533
+ manifest: {
534
+ schemaVersion: 1,
535
+ generatedAt: "2026-03-23T18:20:00.000Z",
536
+ source: null,
537
+ includes: {
538
+ company: false,
539
+ agents: true,
540
+ projects: false,
541
+ issues: false,
542
+ skills: false,
543
+ },
544
+ company: null,
545
+ sidebar: null,
546
+ agents: [
547
+ {
548
+ slug: "legacy-agent",
549
+ name: "Legacy Agent",
550
+ path: "agents/legacy-agent/AGENT.md",
551
+ skills: [],
552
+ role: "agent",
553
+ title: null,
554
+ icon: null,
555
+ capabilities: null,
556
+ reportsToSlug: null,
557
+ adapterType: "process",
558
+ adapterConfig: {},
559
+ runtimeConfig: {},
560
+ permissions: {},
561
+ budgetMonthlyCents: 0,
562
+ metadata: null,
563
+ },
564
+ {
565
+ slug: "explicit-agent",
566
+ name: "Explicit Agent",
567
+ path: "agents/explicit-agent/AGENT.md",
568
+ skills: [],
569
+ role: "agent",
570
+ title: null,
571
+ icon: null,
572
+ capabilities: null,
573
+ reportsToSlug: null,
574
+ adapterType: "codex_local",
575
+ adapterConfig: {},
576
+ runtimeConfig: {},
577
+ permissions: {},
578
+ budgetMonthlyCents: 0,
579
+ metadata: null,
580
+ },
581
+ ],
582
+ skills: [],
583
+ projects: [],
584
+ issues: [],
585
+ envInputs: [],
586
+ },
587
+ files: {},
588
+ envInputs: [],
589
+ warnings: [],
590
+ errors: [],
591
+ };
592
+
593
+ expect(buildDefaultImportAdapterOverrides(preview)).toEqual({
594
+ "legacy-agent": {
595
+ adapterType: "claude_local",
596
+ },
597
+ });
598
+ });
599
+ });
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ defaultClientContext,
7
+ readContext,
8
+ setCurrentProfile,
9
+ upsertProfile,
10
+ writeContext,
11
+ } from "../client/context.js";
12
+
13
+ function createTempContextPath(): string {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-cli-context-"));
15
+ return path.join(dir, "context.json");
16
+ }
17
+
18
+ describe("client context store", () => {
19
+ it("returns default context when file does not exist", () => {
20
+ const contextPath = createTempContextPath();
21
+ const context = readContext(contextPath);
22
+ expect(context).toEqual(defaultClientContext());
23
+ });
24
+
25
+ it("upserts profile values and switches current profile", () => {
26
+ const contextPath = createTempContextPath();
27
+
28
+ upsertProfile(
29
+ "work",
30
+ {
31
+ apiBase: "http://localhost:3100",
32
+ companyId: "company-123",
33
+ apiKeyEnvVarName: "PAPERCLIP_AGENT_TOKEN",
34
+ },
35
+ contextPath,
36
+ );
37
+
38
+ setCurrentProfile("work", contextPath);
39
+ const context = readContext(contextPath);
40
+
41
+ expect(context.currentProfile).toBe("work");
42
+ expect(context.profiles.work).toEqual({
43
+ apiBase: "http://localhost:3100",
44
+ companyId: "company-123",
45
+ apiKeyEnvVarName: "PAPERCLIP_AGENT_TOKEN",
46
+ });
47
+ });
48
+
49
+ it("normalizes invalid file content to safe defaults", () => {
50
+ const contextPath = createTempContextPath();
51
+ writeContext(
52
+ {
53
+ version: 1,
54
+ currentProfile: "x",
55
+ profiles: {
56
+ x: {
57
+ apiBase: " ",
58
+ companyId: " ",
59
+ apiKeyEnvVarName: " ",
60
+ },
61
+ },
62
+ },
63
+ contextPath,
64
+ );
65
+
66
+ const context = readContext(contextPath);
67
+ expect(context.currentProfile).toBe("x");
68
+ expect(context.profiles.x).toEqual({});
69
+ });
70
+ });