@naswerks/periscope 1.0.0 → 1.0.1

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 (187) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/contracts/wire-vectors/bulk_delivered.full.json +21 -0
  4. package/contracts/wire-vectors/bulk_delivered.minimal.json +21 -0
  5. package/contracts/wire-vectors/bulk_delivered.without-stat-pair.json +22 -0
  6. package/contracts/wire-vectors/bulk_failed.declared-reason.json +22 -0
  7. package/contracts/wire-vectors/bulk_failed.unknown-reason.json +23 -0
  8. package/contracts/wire-vectors/bulk_request.full.json +21 -0
  9. package/contracts/wire-vectors/host_configure.set-and-remove.json +28 -0
  10. package/contracts/wire-vectors/host_configure_result.applied.json +34 -0
  11. package/contracts/wire-vectors/host_configure_result.refused.json +33 -0
  12. package/contracts/wire-vectors/link_ack.cursors.json +25 -0
  13. package/contracts/wire-vectors/link_ack.empty.json +16 -0
  14. package/contracts/wire-vectors/link_bye.cause.json +16 -0
  15. package/contracts/wire-vectors/link_hello.empty.json +33 -0
  16. package/contracts/wire-vectors/link_hello.full.json +44 -0
  17. package/contracts/wire-vectors/link_ping.nonce.json +16 -0
  18. package/contracts/wire-vectors/link_ping.unknown-payload-key.json +18 -0
  19. package/contracts/wire-vectors/link_pong.nonce.json +16 -0
  20. package/contracts/wire-vectors/link_welcome.empty.json +18 -0
  21. package/contracts/wire-vectors/link_welcome.full.json +25 -0
  22. package/contracts/wire-vectors/refused.empty.json +10 -0
  23. package/contracts/wire-vectors/refused.encode.empty-at.json +18 -0
  24. package/contracts/wire-vectors/refused.encode.empty-session-id.json +18 -0
  25. package/contracts/wire-vectors/refused.encode.missing-member.json +22 -0
  26. package/contracts/wire-vectors/refused.encode.seq-zero.json +18 -0
  27. package/contracts/wire-vectors/refused.encode.undeclared-configure-refusal.json +34 -0
  28. package/contracts/wire-vectors/refused.encode.undeclared-wire-refusal.json +27 -0
  29. package/contracts/wire-vectors/refused.missing-member.json +10 -0
  30. package/contracts/wire-vectors/refused.not-json.json +10 -0
  31. package/contracts/wire-vectors/refused.over-limit.json +10 -0
  32. package/contracts/wire-vectors/refused.seq-zero.json +10 -0
  33. package/contracts/wire-vectors/refused.unknown-frame.json +10 -0
  34. package/contracts/wire-vectors/refused.unknown-kind.json +10 -0
  35. package/contracts/wire-vectors/repository_list.root.json +19 -0
  36. package/contracts/wire-vectors/repository_list.subdirectory.json +19 -0
  37. package/contracts/wire-vectors/repository_list_result.entries.json +34 -0
  38. package/contracts/wire-vectors/repository_list_result.refused.json +24 -0
  39. package/contracts/wire-vectors/repository_read.head.json +20 -0
  40. package/contracts/wire-vectors/repository_read_result.refused.json +25 -0
  41. package/contracts/wire-vectors/repository_read_result.truncated.json +22 -0
  42. package/contracts/wire-vectors/session_cancel.bare.json +17 -0
  43. package/contracts/wire-vectors/session_configure.full.json +22 -0
  44. package/contracts/wire-vectors/session_configure.minimal.json +20 -0
  45. package/contracts/wire-vectors/session_delta.agent-message.json +27 -0
  46. package/contracts/wire-vectors/session_list.request.json +18 -0
  47. package/contracts/wire-vectors/session_list_result.empty.json +21 -0
  48. package/contracts/wire-vectors/session_list_result.full.json +36 -0
  49. package/contracts/wire-vectors/session_new.full.json +72 -0
  50. package/contracts/wire-vectors/session_new.minimal.json +22 -0
  51. package/contracts/wire-vectors/session_new.request-all-null.json +41 -0
  52. package/contracts/wire-vectors/session_prompt.empty.json +18 -0
  53. package/contracts/wire-vectors/session_prompt.multibyte.json +18 -0
  54. package/contracts/wire-vectors/session_prompt.text.json +18 -0
  55. package/contracts/wire-vectors/session_prompt.unknown-frame-key.json +20 -0
  56. package/contracts/wire-vectors/session_prompt.unknown-payload-key.json +20 -0
  57. package/contracts/wire-vectors/session_update.agent-message.json +26 -0
  58. package/contracts/wire-vectors/session_update.state-transition-with-activity.json +44 -0
  59. package/contracts/wire-vectors/session_update.state-transition.json +41 -0
  60. package/contracts/wire-vectors/session_update.wire-refusal.json +26 -0
  61. package/contracts/wire-vectors/transcript_failed.declared-reason.json +22 -0
  62. package/contracts/wire-vectors/transcript_list.from-start.json +19 -0
  63. package/contracts/wire-vectors/transcript_list.paged.json +19 -0
  64. package/contracts/wire-vectors/transcript_list_result.entry-without-cwd.json +30 -0
  65. package/contracts/wire-vectors/transcript_list_result.full.json +29 -0
  66. package/contracts/wire-vectors/transcript_list_result.last-page.json +29 -0
  67. package/contracts/wire-vectors/transcript_tail.any-user-entry.json +22 -0
  68. package/contracts/wire-vectors/transcript_tail.with-needle.json +22 -0
  69. package/contracts/wire-vectors/transcript_tail_result.absent.json +23 -0
  70. package/contracts/wire-vectors/transcript_tail_result.found.json +23 -0
  71. package/contracts/wire-vectors/workspace_list.from-start.json +19 -0
  72. package/contracts/wire-vectors/workspace_list_result.page.json +48 -0
  73. package/contracts/wire-vectors/workspace_list_result.refused.json +26 -0
  74. package/contracts/wire-vectors/workspace_release.by-path-delete-branch.json +22 -0
  75. package/contracts/wire-vectors/workspace_release.request.json +22 -0
  76. package/contracts/wire-vectors/workspace_release_bulk.two-entries.json +32 -0
  77. package/contracts/wire-vectors/workspace_release_bulk_result.mixed.json +37 -0
  78. package/contracts/wire-vectors/workspace_release_result.partial.json +26 -0
  79. package/contracts/wire-vectors/workspace_release_result.refused.json +26 -0
  80. package/contracts/wire-vectors/workspace_release_result.released.json +23 -0
  81. package/package.json +6 -1
  82. package/src/bin/command.ts +198 -0
  83. package/src/bin/config.ts +104 -0
  84. package/src/bin/login.ts +149 -0
  85. package/src/bin/main.ts +204 -0
  86. package/src/bin/pair.ts +246 -0
  87. package/src/bin/periscope.ts +3 -0
  88. package/src/bin/posture.ts +232 -0
  89. package/src/bin/reconfigure.ts +266 -0
  90. package/src/bin/serve.ts +615 -0
  91. package/src/bin/status.ts +61 -0
  92. package/src/bin/workspaces.ts +280 -0
  93. package/src/control/backoff.ts +40 -0
  94. package/src/control/codec.ts +557 -0
  95. package/src/control/credential.ts +33 -0
  96. package/src/control/frames.ts +1586 -0
  97. package/src/control/link-state.ts +67 -0
  98. package/src/control/link.ts +894 -0
  99. package/src/control/queue.ts +248 -0
  100. package/src/control/seq.ts +76 -0
  101. package/src/control/stream-routing.ts +216 -0
  102. package/src/control/stream.ts +149 -0
  103. package/src/core/async-queue.ts +63 -0
  104. package/src/core/failure.ts +61 -0
  105. package/src/core/index.ts +22 -0
  106. package/src/core/keyed-turns.ts +47 -0
  107. package/src/core/paths.ts +84 -0
  108. package/src/core/refusal.ts +430 -0
  109. package/src/core/result.ts +27 -0
  110. package/src/core/time.ts +34 -0
  111. package/src/core/vocab.ts +44 -0
  112. package/src/core/workspace-id.ts +11 -0
  113. package/src/gate/command.ts +617 -0
  114. package/src/gate/decision.ts +173 -0
  115. package/src/gate/escalate.ts +164 -0
  116. package/src/gate/gate.ts +635 -0
  117. package/src/gate/index.ts +53 -0
  118. package/src/gate/jail.ts +210 -0
  119. package/src/gate/local.ts +115 -0
  120. package/src/gate/outcome.ts +164 -0
  121. package/src/gate/shell.ts +295 -0
  122. package/src/host/agent-process.ts +694 -0
  123. package/src/host/bulk-post.ts +166 -0
  124. package/src/host/claude-transcripts.ts +404 -0
  125. package/src/host/config-file.ts +246 -0
  126. package/src/host/git-facts.ts +119 -0
  127. package/src/host/hooks.ts +102 -0
  128. package/src/host/host.ts +1901 -0
  129. package/src/host/index.ts +92 -0
  130. package/src/host/link-state-file.ts +79 -0
  131. package/src/host/loopback.ts +177 -0
  132. package/src/host/machine.ts +27 -0
  133. package/src/host/mcp-server.ts +66 -0
  134. package/src/host/package-facts.ts +25 -0
  135. package/src/host/paired-credential-store.ts +140 -0
  136. package/src/host/paths.ts +175 -0
  137. package/src/host/repository-read.ts +245 -0
  138. package/src/host/session-store.ts +133 -0
  139. package/src/host/sign-in.ts +352 -0
  140. package/src/host/telemetry.ts +105 -0
  141. package/src/host/token-cache.ts +214 -0
  142. package/src/host/transcript-fs.ts +110 -0
  143. package/src/host/wire-request.ts +285 -0
  144. package/src/host/workspace-fs.ts +90 -0
  145. package/src/host/workspace-trust.ts +92 -0
  146. package/src/identity/authorize.ts +127 -0
  147. package/src/identity/config.ts +227 -0
  148. package/src/identity/credential.ts +176 -0
  149. package/src/identity/device-code.ts +217 -0
  150. package/src/identity/index.ts +84 -0
  151. package/src/identity/mode.ts +161 -0
  152. package/src/identity/paired-credential.ts +89 -0
  153. package/src/identity/pkce.ts +113 -0
  154. package/src/identity/store.ts +98 -0
  155. package/src/identity/token.ts +203 -0
  156. package/src/index.ts +140 -0
  157. package/src/mcp/descriptor.ts +75 -0
  158. package/src/mcp/index.ts +23 -0
  159. package/src/mcp/schema.ts +214 -0
  160. package/src/mcp/server.ts +180 -0
  161. package/src/persistence/entry.ts +142 -0
  162. package/src/persistence/index.ts +47 -0
  163. package/src/persistence/key.ts +77 -0
  164. package/src/persistence/mirror.ts +118 -0
  165. package/src/persistence/receipt.ts +182 -0
  166. package/src/persistence/retention.ts +118 -0
  167. package/src/persistence/store.ts +183 -0
  168. package/src/persistence/transition-log.ts +176 -0
  169. package/src/protocol.ts +215 -0
  170. package/src/sessions/index.ts +27 -0
  171. package/src/sessions/registry.ts +392 -0
  172. package/src/sessions/session.ts +440 -0
  173. package/src/sessions/spawn-env.ts +318 -0
  174. package/src/state/coverage.ts +360 -0
  175. package/src/state/index.ts +58 -0
  176. package/src/state/machine.ts +359 -0
  177. package/src/state/model.ts +417 -0
  178. package/src/state/observer.ts +499 -0
  179. package/src/state/reporter.ts +60 -0
  180. package/src/state/store.ts +157 -0
  181. package/src/telemetry/index.ts +10 -0
  182. package/src/telemetry/usage.ts +117 -0
  183. package/src/workspace/git-worktree.ts +551 -0
  184. package/src/workspace/index.ts +17 -0
  185. package/src/workspace/plain-dir.ts +132 -0
  186. package/src/workspace/provider.ts +159 -0
  187. package/src/workspace/worktree-porcelain.ts +118 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_list",
12
+ "requestId": "request-7",
13
+ "fromIndex": 0
14
+ }
15
+ },
16
+ "name": "workspace_list.from-start",
17
+ "protocolVersion": 9,
18
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_list\",\"requestId\":\"request-7\",\"fromIndex\":0}}"
19
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_list_result",
12
+ "requestId": "request-7",
13
+ "entries": [
14
+ {
15
+ "key": "session-150",
16
+ "path": "/srv/repo-workspaces/session-150",
17
+ "branch": "repo/session-150",
18
+ "head": "2222222222222222222222222222222222222222",
19
+ "detached": false,
20
+ "locked": false,
21
+ "prunable": false,
22
+ "merged": true,
23
+ "aheadCount": 0,
24
+ "lastCommitAt": "2026-09-09T08:30:00+00:00"
25
+ },
26
+ {
27
+ "key": "solo-detached",
28
+ "path": "/srv/repo-workspaces/solo-detached",
29
+ "branch": null,
30
+ "head": null,
31
+ "detached": true,
32
+ "locked": true,
33
+ "prunable": true,
34
+ "merged": null,
35
+ "aheadCount": null,
36
+ "lastCommitAt": null
37
+ }
38
+ ],
39
+ "totalCount": 27,
40
+ "nextIndex": 25,
41
+ "defaultBranch": "main",
42
+ "refusal": null
43
+ }
44
+ },
45
+ "name": "workspace_list_result.page",
46
+ "protocolVersion": 9,
47
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_list_result\",\"requestId\":\"request-7\",\"entries\":[{\"key\":\"session-150\",\"path\":\"/srv/repo-workspaces/session-150\",\"branch\":\"repo/session-150\",\"head\":\"2222222222222222222222222222222222222222\",\"detached\":false,\"locked\":false,\"prunable\":false,\"merged\":true,\"aheadCount\":0,\"lastCommitAt\":\"2026-09-09T08:30:00+00:00\"},{\"key\":\"solo-detached\",\"path\":\"/srv/repo-workspaces/solo-detached\",\"branch\":null,\"head\":null,\"detached\":true,\"locked\":true,\"prunable\":true,\"merged\":null,\"aheadCount\":null,\"lastCommitAt\":null}],\"totalCount\":27,\"nextIndex\":25,\"defaultBranch\":\"main\",\"refusal\":null}}"
48
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_list_result",
12
+ "requestId": "request-7",
13
+ "entries": [],
14
+ "totalCount": 0,
15
+ "nextIndex": null,
16
+ "defaultBranch": null,
17
+ "refusal": {
18
+ "reason": "workspace-list-failed",
19
+ "detail": "no provider"
20
+ }
21
+ }
22
+ },
23
+ "name": "workspace_list_result.refused",
24
+ "protocolVersion": 9,
25
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_list_result\",\"requestId\":\"request-7\",\"entries\":[],\"totalCount\":0,\"nextIndex\":null,\"defaultBranch\":null,\"refusal\":{\"reason\":\"workspace-list-failed\",\"detail\":\"no provider\"}}}"
26
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release",
12
+ "requestId": "request-5",
13
+ "workspaceKey": null,
14
+ "path": "C:/work/workspace-1",
15
+ "deleteBranch": true,
16
+ "force": true
17
+ }
18
+ },
19
+ "name": "workspace_release.by-path-delete-branch",
20
+ "protocolVersion": 9,
21
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release\",\"requestId\":\"request-5\",\"workspaceKey\":null,\"path\":\"C:/work/workspace-1\",\"deleteBranch\":true,\"force\":true}}"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release",
12
+ "requestId": "request-5",
13
+ "workspaceKey": "workspace-1",
14
+ "path": null,
15
+ "deleteBranch": false,
16
+ "force": false
17
+ }
18
+ },
19
+ "name": "workspace_release.request",
20
+ "protocolVersion": 9,
21
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release\",\"requestId\":\"request-5\",\"workspaceKey\":\"workspace-1\",\"path\":null,\"deleteBranch\":false,\"force\":false}}"
22
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release_bulk",
12
+ "requestId": "request-8",
13
+ "releases": [
14
+ {
15
+ "workspaceKey": "workspace-1",
16
+ "path": null,
17
+ "deleteBranch": true,
18
+ "force": false
19
+ },
20
+ {
21
+ "workspaceKey": null,
22
+ "path": "C:/work/workspace-2",
23
+ "deleteBranch": false,
24
+ "force": false
25
+ }
26
+ ]
27
+ }
28
+ },
29
+ "name": "workspace_release_bulk.two-entries",
30
+ "protocolVersion": 9,
31
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release_bulk\",\"requestId\":\"request-8\",\"releases\":[{\"workspaceKey\":\"workspace-1\",\"path\":null,\"deleteBranch\":true,\"force\":false},{\"workspaceKey\":null,\"path\":\"C:/work/workspace-2\",\"deleteBranch\":false,\"force\":false}]}}"
32
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release_bulk_result",
12
+ "requestId": "request-8",
13
+ "results": [
14
+ {
15
+ "workspaceKey": "workspace-1",
16
+ "path": "C:/work/workspace-1",
17
+ "directoryRemoved": true,
18
+ "branchDeleted": true,
19
+ "refusal": null
20
+ },
21
+ {
22
+ "workspaceKey": "workspace-2",
23
+ "path": "C:/work/workspace-2",
24
+ "directoryRemoved": false,
25
+ "branchDeleted": false,
26
+ "refusal": {
27
+ "reason": "branch-not-merged",
28
+ "detail": "not merged into main"
29
+ }
30
+ }
31
+ ]
32
+ }
33
+ },
34
+ "name": "workspace_release_bulk_result.mixed",
35
+ "protocolVersion": 9,
36
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release_bulk_result\",\"requestId\":\"request-8\",\"results\":[{\"workspaceKey\":\"workspace-1\",\"path\":\"C:/work/workspace-1\",\"directoryRemoved\":true,\"branchDeleted\":true,\"refusal\":null},{\"workspaceKey\":\"workspace-2\",\"path\":\"C:/work/workspace-2\",\"directoryRemoved\":false,\"branchDeleted\":false,\"refusal\":{\"reason\":\"branch-not-merged\",\"detail\":\"not merged into main\"}}]}}"
37
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release_result",
12
+ "requestId": "request-5",
13
+ "workspaceKey": "workspace-1",
14
+ "path": "C:/work/workspace-1",
15
+ "directoryRemoved": true,
16
+ "branchDeleted": false,
17
+ "refusal": {
18
+ "reason": "workspace-release-failed",
19
+ "detail": "removed the worktree but could not delete the branch"
20
+ }
21
+ }
22
+ },
23
+ "name": "workspace_release_result.partial",
24
+ "protocolVersion": 9,
25
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release_result\",\"requestId\":\"request-5\",\"workspaceKey\":\"workspace-1\",\"path\":\"C:/work/workspace-1\",\"directoryRemoved\":true,\"branchDeleted\":false,\"refusal\":{\"reason\":\"workspace-release-failed\",\"detail\":\"removed the worktree but could not delete the branch\"}}}"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release_result",
12
+ "requestId": "request-5",
13
+ "workspaceKey": "workspace-1",
14
+ "path": null,
15
+ "directoryRemoved": false,
16
+ "branchDeleted": false,
17
+ "refusal": {
18
+ "reason": "workspace-release-failed",
19
+ "detail": "still backing a live session"
20
+ }
21
+ }
22
+ },
23
+ "name": "workspace_release_result.refused",
24
+ "protocolVersion": 9,
25
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release_result\",\"requestId\":\"request-5\",\"workspaceKey\":\"workspace-1\",\"path\":null,\"directoryRemoved\":false,\"branchDeleted\":false,\"refusal\":{\"reason\":\"workspace-release-failed\",\"detail\":\"still backing a live session\"}}}"
26
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "expect": {
3
+ "decode": "ok"
4
+ },
5
+ "frame": {
6
+ "frame": "session",
7
+ "sessionId": "session-1",
8
+ "seq": 1,
9
+ "at": "2000-01-01T00:00:00.000Z",
10
+ "payload": {
11
+ "kind": "workspace_release_result",
12
+ "requestId": "request-5",
13
+ "workspaceKey": "workspace-1",
14
+ "path": "C:/work/workspace-1",
15
+ "directoryRemoved": true,
16
+ "branchDeleted": true,
17
+ "refusal": null
18
+ }
19
+ },
20
+ "name": "workspace_release_result.released",
21
+ "protocolVersion": 9,
22
+ "wire": "{\"frame\":\"session\",\"sessionId\":\"session-1\",\"seq\":1,\"at\":\"2000-01-01T00:00:00.000Z\",\"payload\":{\"kind\":\"workspace_release_result\",\"requestId\":\"request-5\",\"workspaceKey\":\"workspace-1\",\"path\":\"C:/work/workspace-1\",\"directoryRemoved\":true,\"branchDeleted\":true,\"refusal\":null}}"
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naswerks/periscope",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "description": "Self-hosted runner for agent sessions: dials out to your controller, then spawns, gates, observes, prompts and streams.",
6
6
  "keywords": [
@@ -36,6 +36,11 @@
36
36
  "!dist/**/*.map",
37
37
  "!dist/pins/",
38
38
  "!dist/test-support/",
39
+ "src/",
40
+ "!src/**/*.test.ts",
41
+ "!src/pins/",
42
+ "!src/test-support/",
43
+ "contracts/wire-vectors/",
39
44
  "docs/",
40
45
  "examples/",
41
46
  "README.md",
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Which command the binary was asked for.
3
+ *
4
+ * Extracted out of the composition root so it can be called without starting a host, the same
5
+ * reason, and the same shape, as `workspaces.ts`. `bin/periscope.ts` calls `main()` on load, so
6
+ * importing it to test one function starts a host, opens a socket and spawns nothing useful. The
7
+ * fix is to split the function out; it is not to add a startup guard to the entry point, because
8
+ * changing production behaviour to make a test possible is how a suite starts describing a system
9
+ * nobody ships.
10
+ *
11
+ * The parser is deliberately tiny and not an option library. This package's direct dependencies
12
+ * are the agent SDK with its peer closure, `ws` and `zod`; a CLI framework for a handful of verbs
13
+ * would be a new dependency in a package whose boundary claims are auditable from one directory.
14
+ */
15
+
16
+ /** The verbs this binary answers to. Anything else is named rather than guessed at. */
17
+ export type Command =
18
+ /** Run the host: dial the controller, serve sessions. The default, and what a supervisor starts. */
19
+ | { readonly kind: 'serve' }
20
+ /**
21
+ * Acquire a token interactively and write it to the cache the daemon reads.
22
+ *
23
+ * Why the verb exists: `bin/serve.ts` never calls `signIn`. It builds a credential over the
24
+ * file token cache and a refresher, so it presents a token that is already there, and nothing
25
+ * else in this package ever writes one. An `npx`-installed host on someone's laptop with no way
26
+ * to sign in is not a host; it is a process that reads an empty cache, refuses by name, and
27
+ * (because the link is fail-open) connects with no headers at all.
28
+ */
29
+ | { readonly kind: 'login' }
30
+ /**
31
+ * Trade a pair code for this machine's durable credential and write it beside the token cache.
32
+ *
33
+ * An OIDC refresh token can expire after a period of inactivity, so an unattended host on the
34
+ * `login` credential can quietly stop being able to dial. A paired credential has no clock; it
35
+ * dies only when the controller revokes it, and that refusal is loud (the upgrade 401 is terminal
36
+ * and the process exits naming it). `code: null` is the operator forgetting the argument, named
37
+ * at parse so the message can say what to type.
38
+ */
39
+ | {
40
+ readonly kind: 'pair';
41
+ readonly code: string | null;
42
+ /** `--controller <origin>`: where to redeem, and what the controller names its routes from. */
43
+ readonly controller: string | null;
44
+ /** `--label <name>`: what the controller lists this machine as; overrides the environment. */
45
+ readonly label: string | null;
46
+ /** An argument the parser could not place, named so the usage line can say what to type. */
47
+ readonly problem: string | null;
48
+ }
49
+ /**
50
+ * Read or write the config file the daemon falls back to.
51
+ *
52
+ * Why the verb exists: with every setting readable from the environment only, a user with no UI
53
+ * and no supervisor has no way to configure a repository short of exporting variables into every
54
+ * shell that starts the host. The file is a fallback (the environment still wins per key) and
55
+ * nothing else in this package writes it. `key: null` lists the file; `value: null` reads one
56
+ * key; both stated is a write.
57
+ */
58
+ | {
59
+ readonly kind: 'config';
60
+ readonly key: string | null;
61
+ readonly value: string | null;
62
+ /** `--unset <key>`: remove the key from the file. */
63
+ readonly unset: boolean;
64
+ }
65
+ /** Print the package version. */
66
+ | { readonly kind: 'version' }
67
+ /** Say what the verbs are. */
68
+ | { readonly kind: 'status' }
69
+ | { readonly kind: 'help' }
70
+ /** A verb this binary does not have. Named, never silently treated as `serve`. */
71
+ | { readonly kind: 'unknown'; readonly name: string };
72
+
73
+ /**
74
+ * Read the command from an argv tail (`process.argv.slice(2)`).
75
+ *
76
+ * No arguments means `serve`, and that is a contract rather than a preference: a supervisor or
77
+ * container starts this binary with no arguments, so a bare invocation is the daemon.
78
+ *
79
+ * An unrecognised first argument is `unknown`, never `serve`. Falling back to the default would
80
+ * mean `periscope logn` silently starts a host: the operator believes they are signing in, the
81
+ * process dials out and starts accepting sessions, and the mistake surfaces much later as an empty
82
+ * token cache. A typo must fail loudly at the only moment it is cheap to fix.
83
+ */
84
+ export function readCommand(argv: readonly string[]): Command {
85
+ const first = argv[0];
86
+
87
+ if (first === undefined || first === '') return { kind: 'serve' };
88
+
89
+ switch (first) {
90
+ case 'serve':
91
+ return { kind: 'serve' };
92
+ case 'login':
93
+ return { kind: 'login' };
94
+ case 'pair':
95
+ return readPairArguments(argv.slice(1));
96
+ case 'config': {
97
+ if (argv[1] === '--unset') {
98
+ const key = argv[2];
99
+ return {
100
+ kind: 'config',
101
+ key: key === undefined || key === '' ? null : key,
102
+ value: null,
103
+ unset: true,
104
+ };
105
+ }
106
+ const key = argv[1];
107
+ const value = argv[2];
108
+ return {
109
+ kind: 'config',
110
+ key: key === undefined || key === '' ? null : key,
111
+ value: value === undefined || value === '' ? null : value,
112
+ unset: false,
113
+ };
114
+ }
115
+ case 'version':
116
+ case '--version':
117
+ case '-v':
118
+ return { kind: 'version' };
119
+ case 'status':
120
+ return { kind: 'status' };
121
+ case 'help':
122
+ case '--help':
123
+ case '-h':
124
+ return { kind: 'help' };
125
+ default:
126
+ return { kind: 'unknown', name: first };
127
+ }
128
+ }
129
+
130
+ /** The two flags `pair` takes. A closed set: anything else starting with `--` is a problem, never the code. */
131
+ const PAIR_FLAGS = ['--controller', '--label'] as const;
132
+
133
+ /**
134
+ * `pair`'s tail: one positional (the code) and two optional flags, each as `--flag value` or
135
+ * `--flag=value`. Hand-rolled for the same reason the whole parser is: two flags do not earn an
136
+ * option library. An unknown `--x` is a problem rather than the code, so a typo'd flag cannot be
137
+ * sent to the controller as a pair code and refused there with the wrong message.
138
+ */
139
+ function readPairArguments(rest: readonly string[]): Command {
140
+ let code: string | null = null;
141
+ let controller: string | null = null;
142
+ let label: string | null = null;
143
+ let problem: string | null = null;
144
+
145
+ for (let index = 0; index < rest.length; index += 1) {
146
+ const argument = rest[index] ?? '';
147
+ if (argument.startsWith('--')) {
148
+ const equals = argument.indexOf('=');
149
+ const name = equals === -1 ? argument : argument.slice(0, equals);
150
+ if (!(PAIR_FLAGS as readonly string[]).includes(name)) {
151
+ problem ??= `unknown option '${name}'`;
152
+ continue;
153
+ }
154
+ let value: string | undefined;
155
+ if (equals === -1) {
156
+ value = rest[index + 1];
157
+ index += 1;
158
+ } else {
159
+ value = argument.slice(equals + 1);
160
+ }
161
+ if (value === undefined || value === '') {
162
+ problem ??= `${name} needs a value`;
163
+ continue;
164
+ }
165
+ if (name === '--controller') controller = value;
166
+ else label = value;
167
+ continue;
168
+ }
169
+ if (argument === '') continue;
170
+ if (code === null) code = argument;
171
+ else problem ??= `unexpected argument '${argument}'`;
172
+ }
173
+
174
+ return { kind: 'pair', code, controller, label, problem };
175
+ }
176
+
177
+ /** What `help` prints. Kept beside the parser so a new verb cannot be added without a line here. */
178
+ export const USAGE = [
179
+ 'periscope — hosts Claude Code sessions for a remote controller.',
180
+ '',
181
+ 'Usage:',
182
+ ' periscope [serve] Dial the controller and serve sessions (the default).',
183
+ ' periscope login Sign in and write the token cache this host presents.',
184
+ ' periscope pair <code> [--controller <origin>] [--label <name>]',
185
+ " Trade a pair code for this machine's durable credential. The controller",
186
+ ' origin names where to redeem and writes the link URLs to the config file;',
187
+ ' the label is what the controller lists this machine as.',
188
+ ' periscope config Show the config file and every value in it.',
189
+ ' periscope config <key> [value] Read one config value, or write it.',
190
+ ' periscope config --unset <key> Remove one config value from the file.',
191
+ ' periscope status Say what this host is: its link, credential, ids, workspace and where',
192
+ ' each setting came from. Reads what serve left behind; never dials.',
193
+ ' periscope version Print the package version.',
194
+ ' periscope help Show this.',
195
+ '',
196
+ 'Configuration is read from the environment first, then the config file (the environment wins);',
197
+ 'see README.md.',
198
+ ].join('\n');
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `periscope config` — read or write the config file the daemon falls back to.
3
+ *
4
+ * Why this is a command: the binary reads its configuration from the environment first and the
5
+ * file second (every consuming verb: `serve`, `login`, `pair`), and nothing else in this package
6
+ * ever writes that file. Without it, a user installing the host on a machine with no UI has
7
+ * exactly one configuration surface, env vars at process start, which is a supervisor's surface,
8
+ * not a person's. This verb is the person's.
9
+ *
10
+ * It orchestrates only. The filesystem work lives in `host/config-file.ts`, because `src/host/`
11
+ * is the one directory allowed to touch the machine; this module composes that seam and formats
12
+ * lines, exactly the split `login` and `pair` use.
13
+ *
14
+ * The output says what wins. Every listing line marks a value the environment currently
15
+ * overrides, because "set it and nothing changed" with a stale env var standing in front of the
16
+ * file is otherwise confusing; the rule is env-wins and the listing repeats it where the operator
17
+ * is looking.
18
+ */
19
+ import {
20
+ CONFIG_KEYS,
21
+ configFilePath,
22
+ readConfigFile,
23
+ writeConfigEntries,
24
+ writeConfigEntry,
25
+ } from '../host/config-file.js';
26
+ import { configValueProblem } from './reconfigure.js';
27
+
28
+ /** What one invocation produced: the lines to print, and whether it succeeded. */
29
+ export interface ConfigOutcome {
30
+ readonly ok: boolean;
31
+ readonly lines: readonly string[];
32
+ }
33
+
34
+ export function runConfig(
35
+ key: string | null,
36
+ value: string | null,
37
+ env: NodeJS.ProcessEnv,
38
+ unset = false,
39
+ ): ConfigOutcome {
40
+ const path = configFilePath(env);
41
+ if (path === null) {
42
+ return {
43
+ ok: false,
44
+ lines: ['there is nowhere to keep a config file — no home directory and no PERISCOPE_CONFIG_DIR'],
45
+ };
46
+ }
47
+
48
+ if (unset) {
49
+ if (key === null) return { ok: false, lines: ['usage: periscope config --unset <key>'] };
50
+ const reading = readConfigFile(env);
51
+ if (reading.problem !== null) return { ok: false, lines: [reading.problem] };
52
+ if (reading.values[key] === undefined) return { ok: true, lines: [`${key} is not set in ${path}`] };
53
+ const problem = writeConfigEntries(env, [{ key, value: null }]);
54
+ if (problem !== null) return { ok: false, lines: [problem] };
55
+ return { ok: true, lines: [`${key} removed from ${path}`] };
56
+ }
57
+
58
+ if (key === null) {
59
+ const reading = readConfigFile(env);
60
+ if (reading.problem !== null) return { ok: false, lines: [reading.problem] };
61
+ const entries = Object.entries(reading.values);
62
+ const lines = [
63
+ `config file: ${path}`,
64
+ ...(entries.length === 0 ? ['(empty — set a value with: periscope config <key> <value>)'] : []),
65
+ ...entries.map(([name, stored]) => {
66
+ const fromEnv = env[name];
67
+ const overridden =
68
+ fromEnv !== undefined && fromEnv !== '' ? ' (currently overridden by the environment)' : '';
69
+ return ` ${name} = ${stored}${overridden}`;
70
+ }),
71
+ `keys this host reads: ${CONFIG_KEYS.join(', ')}`,
72
+ ];
73
+ return { ok: true, lines };
74
+ }
75
+
76
+ if (value === null) {
77
+ const reading = readConfigFile(env);
78
+ if (reading.problem !== null) return { ok: false, lines: [reading.problem] };
79
+ const stored = reading.values[key];
80
+ if (stored === undefined) {
81
+ return { ok: false, lines: [`${key} is not set in ${path}`] };
82
+ }
83
+ return { ok: true, lines: [stored] };
84
+ }
85
+
86
+ // The same screen the wire runs on `host_configure`, before anything is written: a value the
87
+ // daemon would refuse at its next start is refused here, where the person typing it is looking.
88
+ const screened = configValueProblem(key, value);
89
+ if (screened !== null) return { ok: false, lines: [screened] };
90
+ const problem = writeConfigEntry(env, key, value);
91
+ if (problem !== null) return { ok: false, lines: [problem] };
92
+ const fromEnv = env[key];
93
+ return {
94
+ ok: true,
95
+ lines: [
96
+ `${key} written to ${path}`,
97
+ ...(fromEnv !== undefined && fromEnv !== ''
98
+ ? [
99
+ `note: ${key} is currently set in the environment, and the environment wins — the file value is a fallback`,
100
+ ]
101
+ : []),
102
+ ],
103
+ };
104
+ }