@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.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 (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,231 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ import { reconcileBaseSkills } from './base-skills.js';
7
+
8
+ const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
9
+ const DEFAULT_LOCK_STALE_MS = 10 * 60_000;
10
+ const DEFAULT_LOCK_POLL_MS = 50;
11
+
12
+ async function lockSnapshot(lockDirectory) {
13
+ try {
14
+ const [raw, metadata] = await Promise.all([
15
+ readFile(join(lockDirectory, 'owner'), 'utf8').catch(() => ''),
16
+ stat(lockDirectory),
17
+ ]);
18
+ let owner = {};
19
+ try {
20
+ owner = raw ? JSON.parse(raw) : {};
21
+ } catch {
22
+ // A process can die between mkdir and the atomic owner write. Preserve
23
+ // the directory mtime as a reclaimable ownerless lease.
24
+ }
25
+ return {
26
+ id: typeof owner.id === 'string' ? owner.id : null,
27
+ pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
28
+ at: Number.isFinite(Number(owner.at)) ? Number(owner.at) : metadata.mtimeMs,
29
+ mtimeMs: metadata.mtimeMs,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function sameLockSnapshot(left, right) {
37
+ return Boolean(
38
+ left
39
+ && right
40
+ && left.id === right.id
41
+ && left.pid === right.pid
42
+ && left.at === right.at
43
+ && left.mtimeMs === right.mtimeMs,
44
+ );
45
+ }
46
+
47
+ function processIsAlive(pid) {
48
+ if (!Number.isInteger(pid) || pid <= 0) return false;
49
+ try {
50
+ process.kill(pid, 0);
51
+ return true;
52
+ } catch (error) {
53
+ return error?.code === 'EPERM';
54
+ }
55
+ }
56
+
57
+ function delay(milliseconds) {
58
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
59
+ }
60
+
61
+ async function quarantineStaleLock(lockDirectory, snapshot) {
62
+ const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
63
+ const snapshotToken = createHash('sha256')
64
+ .update(JSON.stringify(snapshot))
65
+ .digest('hex');
66
+ await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
67
+ try {
68
+ // The deterministic destination is the compare-and-swap guard. If two
69
+ // waiters observed the same stale owner, only one can move it here. The
70
+ // retained non-empty tombstone prevents the loser from ever moving a new
71
+ // live lock that appeared at the shared pathname in the meantime.
72
+ await rename(lockDirectory, join(quarantineRoot, snapshotToken));
73
+ return true;
74
+ } catch (error) {
75
+ if (['EEXIST', 'ENOTEMPTY', 'ENOENT'].includes(error?.code)) return false;
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ async function writeLockOwnerAtomically(lockDirectory, owner) {
81
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${owner.id}.tmp`);
82
+ const ownerPath = join(lockDirectory, 'owner');
83
+ await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 0o600 });
84
+ await rename(temporaryOwnerPath, ownerPath);
85
+ }
86
+
87
+ async function releaseOwnedLock(lockDirectory, ownerId) {
88
+ const owner = await lockSnapshot(lockDirectory);
89
+ if (owner?.id !== ownerId) return;
90
+
91
+ const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
92
+ const releasedDirectory = join(quarantineRoot, `released.${ownerId}`);
93
+ await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
94
+ try {
95
+ // Moving the owned directory releases the shared pathname atomically.
96
+ // Delete only the private destination so a waiter can safely acquire a new
97
+ // lock without racing this owner's recursive cleanup.
98
+ await rename(lockDirectory, releasedDirectory);
99
+ } catch (error) {
100
+ if (error?.code === 'ENOENT') return;
101
+ throw error;
102
+ }
103
+ await rm(releasedDirectory, { recursive: true, force: true });
104
+ }
105
+
106
+ /** Serialize Desktop and terminal skill sync across processes on one Mac. */
107
+ export async function withSkillSyncLock(callback, {
108
+ home = homedir(),
109
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
110
+ staleMs = DEFAULT_LOCK_STALE_MS,
111
+ pollMs = DEFAULT_LOCK_POLL_MS,
112
+ now = () => Date.now(),
113
+ } = {}) {
114
+ const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
115
+ const ownerId = `${process.pid}.${randomUUID()}`;
116
+ const deadline = now() + timeoutMs;
117
+ await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
118
+
119
+ for (;;) {
120
+ try {
121
+ await mkdir(lockDirectory, { mode: 0o700 });
122
+ try {
123
+ await writeLockOwnerAtomically(lockDirectory, {
124
+ id: ownerId,
125
+ pid: process.pid,
126
+ at: now(),
127
+ });
128
+ } catch (error) {
129
+ // This process exclusively created the directory and has not yet
130
+ // published an owner, so it is safe to undo a failed acquisition.
131
+ await rm(lockDirectory, { recursive: true, force: true });
132
+ throw error;
133
+ }
134
+ break;
135
+ } catch (error) {
136
+ if (error?.code !== 'EEXIST') throw error;
137
+ }
138
+
139
+ const observed = await lockSnapshot(lockDirectory);
140
+ if (
141
+ observed
142
+ && now() - observed.at > staleMs
143
+ && !processIsAlive(observed.pid)
144
+ ) {
145
+ // Require an unchanged observation interval before stealing. This avoids
146
+ // racing a holder that is publishing or refreshing its owner lease.
147
+ await delay(Math.max(pollMs, 10));
148
+ const current = await lockSnapshot(lockDirectory);
149
+ if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
150
+ if (await quarantineStaleLock(lockDirectory, current)) continue;
151
+ }
152
+ }
153
+ if (now() >= deadline) {
154
+ throw new Error('Timed out waiting for another Notis skill sync to finish.');
155
+ }
156
+ await delay(pollMs);
157
+ }
158
+
159
+ const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
160
+ let heartbeatStopped = false;
161
+ let heartbeatInFlight = Promise.resolve();
162
+ const refreshHeartbeat = async () => {
163
+ if (heartbeatStopped) return;
164
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
165
+ await writeFile(
166
+ temporaryOwnerPath,
167
+ JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
168
+ { mode: 0o600 },
169
+ );
170
+ const owner = await lockSnapshot(lockDirectory);
171
+ if (owner?.id !== ownerId) {
172
+ await rm(temporaryOwnerPath, { force: true });
173
+ return;
174
+ }
175
+ // If a stale-lock reclaimer moved this directory after the ownership
176
+ // check, the source path disappears and rename fails instead of
177
+ // overwriting the owner of a newly acquired lock at the shared path.
178
+ await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
179
+ };
180
+ const heartbeat = setInterval(() => {
181
+ // Serialize refreshes and retain the active promise so cleanup cannot race
182
+ // a temporary owner write that was already in flight when the interval was
183
+ // cleared.
184
+ heartbeatInFlight = heartbeatInFlight
185
+ .then(refreshHeartbeat)
186
+ .catch(() => undefined);
187
+ }, heartbeatMs);
188
+ heartbeat.unref?.();
189
+
190
+ try {
191
+ return await callback();
192
+ } finally {
193
+ heartbeatStopped = true;
194
+ clearInterval(heartbeat);
195
+ await heartbeatInFlight;
196
+ await releaseOwnedLock(lockDirectory, ownerId);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Shared CLI-owned orchestration for base-skill installation plus account sync.
202
+ * Desktop injects the source engine that webpack bundles; the CLI command
203
+ * injects the generated dist engine used by the published package.
204
+ */
205
+ export async function reconcileAllSkills({
206
+ serverUrl,
207
+ jwt,
208
+ honorSyncEnabled,
209
+ userId = null,
210
+ home,
211
+ runAccountSync,
212
+ reconcileBase = reconcileBaseSkills,
213
+ lockOptions = {},
214
+ }) {
215
+ return withSkillSyncLock(async () => {
216
+ const base = reconcileBase({ userId, ...(home ? { home } : {}) });
217
+ const account = await runAccountSync(
218
+ serverUrl,
219
+ jwt,
220
+ {},
221
+ { honorSyncEnabled, ...(home ? { syncHome: home } : {}) },
222
+ );
223
+ return {
224
+ ...account,
225
+ baseSkills: base.skills,
226
+ baseInstalled: base.installed,
227
+ baseLinked: base.linked,
228
+ baseBackups: base.backups,
229
+ };
230
+ }, { ...lockOptions, ...(home ? { home } : {}) });
231
+ }
@@ -0,0 +1,92 @@
1
+ const ERROR_CATEGORIES = new Map([
2
+ [2, 'usage'],
3
+ [3, 'auth'],
4
+ [4, 'network'],
5
+ [5, 'conflict'],
6
+ [6, 'backend'],
7
+ [7, 'unexpected'],
8
+ [8, 'payment'],
9
+ ]);
10
+
11
+ export function cliDurationBucket(durationMs) {
12
+ if (durationMs < 1_000) return 'under_1s';
13
+ if (durationMs < 5_000) return '1s_to_5s';
14
+ if (durationMs < 30_000) return '5s_to_30s';
15
+ return 'over_30s';
16
+ }
17
+
18
+ export function cliBackendKind(spec) {
19
+ const value = String(spec?.backend_call?.type || '').toLowerCase();
20
+ if (value === 'tool') return 'tool';
21
+ if (value === 'http') return 'http';
22
+ if (value === 'local') return 'local';
23
+ if (value.includes('health')) return 'health';
24
+ if (value.includes('local')) return 'local';
25
+ return 'other';
26
+ }
27
+
28
+ export function buildCliTelemetryPayload({
29
+ spec,
30
+ runtime,
31
+ result,
32
+ durationMs,
33
+ error,
34
+ }) {
35
+ return {
36
+ command_id: spec.command_path.join('.'),
37
+ backend_kind: cliBackendKind(spec),
38
+ result,
39
+ error_category: result === 'failed'
40
+ ? (ERROR_CATEGORIES.get(Number(error?.exitCode)) || 'unexpected')
41
+ : 'none',
42
+ duration_bucket: cliDurationBucket(durationMs),
43
+ cli_version: runtime.cliVersion,
44
+ agent_mode: Boolean(runtime.agentMode),
45
+ };
46
+ }
47
+
48
+ export async function reportCliCommand({
49
+ spec,
50
+ runtime,
51
+ result,
52
+ durationMs,
53
+ error = null,
54
+ fetchImpl = globalThis.fetch,
55
+ }) {
56
+ // Backend tool calls emit authoritative surface/tool telemetry server-side.
57
+ // This client event exists only for local commands that otherwise leave no
58
+ // observable runtime boundary.
59
+ if (
60
+ cliBackendKind(spec) !== 'local'
61
+ || !runtime?.jwt
62
+ || !runtime?.apiBase
63
+ || typeof fetchImpl !== 'function'
64
+ ) {
65
+ return false;
66
+ }
67
+ const controller = new AbortController();
68
+ const timeout = setTimeout(() => controller.abort(), 1_500);
69
+ try {
70
+ const response = await fetchImpl(`${runtime.apiBase}/cli_telemetry`, {
71
+ method: 'POST',
72
+ headers: {
73
+ Authorization: `Bearer ${runtime.jwt}`,
74
+ 'Content-Type': 'application/json',
75
+ 'X-Notis-CLI-Version': runtime.cliVersion,
76
+ },
77
+ body: JSON.stringify(buildCliTelemetryPayload({
78
+ spec,
79
+ runtime,
80
+ result,
81
+ durationMs,
82
+ error,
83
+ })),
84
+ signal: controller.signal,
85
+ });
86
+ return response.ok;
87
+ } catch {
88
+ return false;
89
+ } finally {
90
+ clearTimeout(timeout);
91
+ }
92
+ }