@notis_ai/cli 0.2.6 → 0.2.8
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.
- package/README.md +17 -0
- package/dist/scaffolds/notis-database/app/page.tsx +38 -41
- package/dist/scaffolds/notis-database/lib/types.ts +5 -7
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
- package/dist/scaffolds/notis-database/notis.config.ts +48 -1
- package/dist/scaffolds/notis-database/package-lock.json +3935 -0
- package/dist/scaffolds/notis-database/package.json +2 -1
- package/dist/scaffolds/notis-database/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
- package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
- package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
- package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
- package/dist/scaffolds/notis-journal/package.json +2 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
- package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
- package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
- package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
- package/dist/scaffolds/notis-notes/package.json +7 -3
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
- package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
- package/dist/scaffolds/notis-random/app/page.tsx +141 -74
- package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
- package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
- package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
- package/dist/scaffolds/notis-random/components.json +20 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
- package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
- package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
- package/dist/scaffolds/notis-random/notis.config.ts +47 -3
- package/dist/scaffolds/notis-random/package-lock.json +4513 -0
- package/dist/scaffolds/notis-random/package.json +6 -2
- package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
- package/dist/scaffolds.json +8 -6
- package/package.json +2 -1
- package/skills/notis-apps/SKILL.md +449 -144
- package/skills/notis-apps/cli.md +15 -0
- package/skills/notis-cli/SKILL.md +8 -4
- package/skills/notis-onboarding/BRIEF.md +93 -0
- package/src/cli.js +52 -7
- package/src/command-specs/apps.js +154 -21
- package/src/command-specs/diagnostics.js +674 -0
- package/src/command-specs/helpers.js +4 -1
- package/src/command-specs/index.js +8 -0
- package/src/command-specs/onboarding.js +216 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +189 -5
- package/src/runtime/app-platform.js +65 -4
- package/src/runtime/desktop-auth.js +66 -17
- package/src/runtime/errors.js +1 -0
- package/src/runtime/output.js +21 -3
- package/src/runtime/profiles.js +214 -18
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +29 -1
- package/template/.harness/index.html.tmpl +30 -2
- package/template/packages/sdk/src/config.ts +38 -1
package/src/runtime/profiles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { dirname, join, parse, resolve } from 'node:path';
|
|
4
4
|
import { CliError, EXIT_CODES } from './errors.js';
|
|
5
5
|
import { getDesktopAuthRecovery } from './desktop-auth.js';
|
|
6
6
|
|
|
@@ -9,6 +9,8 @@ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
|
9
9
|
export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
|
|
10
10
|
export const DEFAULT_API_BASE = 'https://api.notis.ai';
|
|
11
11
|
export const DEFAULT_PROFILE = 'default';
|
|
12
|
+
const WORKTREE_RUNTIME_FILENAME = join('.context', 'notis-runtime.json');
|
|
13
|
+
const WORKTREE_ROUTING_FILENAME = join('.context', 'notis-routing.json');
|
|
12
14
|
const LOCAL_DEFAULT_API_BASES = new Set([
|
|
13
15
|
'http://localhost:3001',
|
|
14
16
|
'http://127.0.0.1:3001',
|
|
@@ -77,17 +79,129 @@ export function normalizeConfig(rawConfig = {}) {
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
function readJsonFile(path) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findUp(filename, startDir = process.cwd()) {
|
|
91
|
+
let current = resolve(startDir);
|
|
92
|
+
const root = parse(current).root;
|
|
93
|
+
while (true) {
|
|
94
|
+
const candidate = join(current, filename);
|
|
95
|
+
if (existsSync(candidate)) {
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
if (current === root) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
current = dirname(current);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function processIsAlive(pid) {
|
|
106
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveWorktreeRuntime(startDir = process.cwd()) {
|
|
118
|
+
if (
|
|
119
|
+
process.env.NODE_ENV === 'test' &&
|
|
120
|
+
process.env.NOTIS_TEST_DISABLE_WORKTREE_ROUTING === '1'
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const runtimePath = findUp(WORKTREE_RUNTIME_FILENAME, startDir);
|
|
125
|
+
const routingPath = findUp(WORKTREE_ROUTING_FILENAME, startDir);
|
|
126
|
+
const routing = routingPath ? readJsonFile(routingPath) : null;
|
|
127
|
+
|
|
128
|
+
if (!runtimePath) {
|
|
129
|
+
if (routing?.mode === 'local-only') {
|
|
130
|
+
throw new CliError({
|
|
131
|
+
code: 'dev_runtime_unavailable',
|
|
132
|
+
message: `This worktree is local-only, but its dev.sh runtime is not active`,
|
|
133
|
+
exitCode: EXIT_CODES.network,
|
|
134
|
+
hints: [
|
|
135
|
+
{ message: 'Start ./dev.sh in this worktree, then retry the command.' },
|
|
136
|
+
{ message: `Routing policy: ${routingPath}` },
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const runtime = readJsonFile(runtimePath);
|
|
144
|
+
const apiBase = typeof runtime?.api_base === 'string' ? runtime.api_base.replace(/\/+$/, '') : '';
|
|
145
|
+
const configFile = typeof runtime?.config_file === 'string' ? runtime.config_file : '';
|
|
146
|
+
const pid = Number(runtime?.dev_pid);
|
|
147
|
+
if (
|
|
148
|
+
runtime?.mode !== 'local-only' ||
|
|
149
|
+
!isLocalApiBase(apiBase) ||
|
|
150
|
+
!configFile ||
|
|
151
|
+
!processIsAlive(pid)
|
|
152
|
+
) {
|
|
153
|
+
throw new CliError({
|
|
154
|
+
code: 'dev_runtime_unavailable',
|
|
155
|
+
message: 'The local-only worktree runtime is stale or invalid',
|
|
156
|
+
exitCode: EXIT_CODES.network,
|
|
157
|
+
hints: [
|
|
158
|
+
{ message: 'Restart ./dev.sh in this worktree, then retry the command.' },
|
|
159
|
+
{ message: `Runtime lease: ${runtimePath}` },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...runtime,
|
|
166
|
+
api_base: apiBase,
|
|
167
|
+
config_file: resolve(dirname(runtimePath), configFile),
|
|
168
|
+
runtime_path: runtimePath,
|
|
169
|
+
routing_path: routingPath,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveConfigFile(runtime = null) {
|
|
174
|
+
if (runtime?.config_file) {
|
|
175
|
+
return runtime.config_file;
|
|
176
|
+
}
|
|
177
|
+
const envConfigFile = process.env.NOTIS_CLI_CONFIG_FILE;
|
|
178
|
+
if (envConfigFile) {
|
|
179
|
+
return resolve(envConfigFile);
|
|
180
|
+
}
|
|
181
|
+
return CONFIG_FILE;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function loadConfig(runtime = null) {
|
|
185
|
+
const configFile = resolveConfigFile(runtime);
|
|
186
|
+
if (!existsSync(configFile)) {
|
|
82
187
|
return normalizeConfig({});
|
|
83
188
|
}
|
|
84
189
|
|
|
85
|
-
|
|
190
|
+
// The desktop app rewrites this file while the CLI may be reading it, so a
|
|
191
|
+
// torn or corrupt read is expected rather than exceptional. Degrade to
|
|
192
|
+
// "unauthenticated", which surfaces the actionable auth_missing error instead
|
|
193
|
+
// of a raw SyntaxError from deep inside the runtime.
|
|
194
|
+
try {
|
|
195
|
+
return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
|
|
196
|
+
} catch {
|
|
197
|
+
return normalizeConfig({});
|
|
198
|
+
}
|
|
86
199
|
}
|
|
87
200
|
|
|
88
|
-
export function saveConfig(config) {
|
|
89
|
-
|
|
90
|
-
|
|
201
|
+
export function saveConfig(config, runtime = null) {
|
|
202
|
+
const configFile = resolveConfigFile(runtime);
|
|
203
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
204
|
+
writeFileSync(configFile, JSON.stringify(normalizeConfig(config), null, 2), { mode: 0o600 });
|
|
91
205
|
}
|
|
92
206
|
|
|
93
207
|
export function getProfile(config, profileName) {
|
|
@@ -194,23 +308,75 @@ export function resolveTimeoutMs(globalOptions = {}) {
|
|
|
194
308
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
195
309
|
}
|
|
196
310
|
|
|
197
|
-
export function
|
|
198
|
-
|
|
311
|
+
export function parseDebugEntitlementOverride(value = process.env.NOTIS_DEBUG_ENTITLEMENT_OVERRIDE) {
|
|
312
|
+
if (!value) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const candidates = [value];
|
|
316
|
+
try {
|
|
317
|
+
candidates.push(Buffer.from(value, 'base64url').toString('utf-8'));
|
|
318
|
+
} catch {
|
|
319
|
+
// The plain JSON candidate below will provide the actionable error.
|
|
320
|
+
}
|
|
321
|
+
for (const candidate of candidates) {
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(candidate);
|
|
324
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Try the next supported encoding.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw new CliError({
|
|
332
|
+
code: 'debug_entitlement_override_invalid',
|
|
333
|
+
message: 'NOTIS_DEBUG_ENTITLEMENT_OVERRIDE must be a JSON object or base64url-encoded JSON object.',
|
|
334
|
+
exitCode: EXIT_CODES.usage,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function resolveRuntimeProfile(
|
|
339
|
+
globalOptions = {},
|
|
340
|
+
{ requireAuth = true, includeDebugEntitlementOverride = true } = {},
|
|
341
|
+
) {
|
|
342
|
+
const worktreeRuntime = resolveWorktreeRuntime();
|
|
343
|
+
const config = loadConfig(worktreeRuntime);
|
|
199
344
|
const profileName = getCurrentProfileName(config, globalOptions.profile);
|
|
200
|
-
const
|
|
201
|
-
|
|
345
|
+
const requestedApiBase = globalOptions.apiBase;
|
|
346
|
+
if (
|
|
347
|
+
worktreeRuntime &&
|
|
348
|
+
requestedApiBase &&
|
|
349
|
+
requestedApiBase.replace(/\/+$/, '') !== worktreeRuntime.api_base
|
|
350
|
+
) {
|
|
351
|
+
throw new CliError({
|
|
352
|
+
code: 'dev_runtime_route_mismatch',
|
|
353
|
+
message: `This local-only worktree cannot route to ${requestedApiBase}`,
|
|
354
|
+
exitCode: EXIT_CODES.usage,
|
|
355
|
+
hints: [{ message: `Expected local API: ${worktreeRuntime.api_base}` }],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const apiBase = worktreeRuntime
|
|
359
|
+
? worktreeRuntime.api_base
|
|
360
|
+
: getApiBase(config, profileName, globalOptions.apiBase);
|
|
361
|
+
const jwt = worktreeRuntime ? getProfile(config, profileName).jwt : getJwt(config, profileName);
|
|
202
362
|
const profile = getProfile(config, profileName);
|
|
203
363
|
const agentMode = isAgentMode(globalOptions);
|
|
204
364
|
const nonInteractive = isNonInteractive(globalOptions);
|
|
205
365
|
const outputMode = resolveOutputMode(globalOptions);
|
|
206
366
|
const timeoutMs = resolveTimeoutMs(globalOptions);
|
|
367
|
+
const debugEntitlementOverride = includeDebugEntitlementOverride
|
|
368
|
+
? parseDebugEntitlementOverride()
|
|
369
|
+
: null;
|
|
207
370
|
|
|
208
371
|
if (requireAuth && !jwt) {
|
|
209
|
-
const recovery = getDesktopAuthRecovery(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
372
|
+
const recovery = getDesktopAuthRecovery(
|
|
373
|
+
{
|
|
374
|
+
apiBase,
|
|
375
|
+
desktopAppName: profile.desktop_app_name,
|
|
376
|
+
desktopPid: profile.desktop_pid,
|
|
377
|
+
},
|
|
378
|
+
{ mode: 'missing' },
|
|
379
|
+
);
|
|
214
380
|
throw new CliError({
|
|
215
381
|
code: 'auth_missing',
|
|
216
382
|
message: `No JWT configured for profile ${profileName}`,
|
|
@@ -218,22 +384,38 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
|
|
|
218
384
|
hints: recovery.hints,
|
|
219
385
|
});
|
|
220
386
|
}
|
|
387
|
+
if (
|
|
388
|
+
worktreeRuntime?.expected_user_id &&
|
|
389
|
+
getJwtSubject(jwt) !== worktreeRuntime.expected_user_id
|
|
390
|
+
) {
|
|
391
|
+
throw new CliError({
|
|
392
|
+
code: 'dev_runtime_identity_mismatch',
|
|
393
|
+
message: 'The scoped dev credential does not belong to this worktree test user',
|
|
394
|
+
exitCode: EXIT_CODES.auth,
|
|
395
|
+
hints: [
|
|
396
|
+
{ message: 'Restart ./dev.sh to restore the approved worktree identity.' },
|
|
397
|
+
{ message: `Expected user: ${worktreeRuntime.expected_user_id}` },
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
221
401
|
|
|
222
402
|
// An explicit NOTIS_JWT is a complete credential override. Use it verbatim
|
|
223
403
|
// and never replace it with a token later synced by the desktop profile.
|
|
224
|
-
const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
|
|
404
|
+
const usingEnvJwt = !worktreeRuntime && Boolean(process.env.NOTIS_JWT);
|
|
225
405
|
return {
|
|
226
406
|
config,
|
|
227
407
|
profileName,
|
|
228
408
|
apiBase,
|
|
229
409
|
jwt,
|
|
230
|
-
credentialSource: usingEnvJwt ? 'env' : 'profile',
|
|
410
|
+
credentialSource: worktreeRuntime ? 'worktree' : usingEnvJwt ? 'env' : 'profile',
|
|
231
411
|
desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
|
|
232
412
|
desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
|
|
233
413
|
agentMode,
|
|
234
414
|
nonInteractive,
|
|
235
415
|
outputMode,
|
|
236
416
|
timeoutMs,
|
|
417
|
+
debugEntitlementOverride,
|
|
418
|
+
worktreeRuntime,
|
|
237
419
|
};
|
|
238
420
|
}
|
|
239
421
|
|
|
@@ -251,6 +433,20 @@ export function getJwtExpiration(jwt) {
|
|
|
251
433
|
}
|
|
252
434
|
}
|
|
253
435
|
|
|
436
|
+
export function getJwtSubject(jwt) {
|
|
437
|
+
if (typeof jwt !== 'string' || !jwt) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
const parts = jwt.split('.');
|
|
442
|
+
if (parts.length !== 3) return null;
|
|
443
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
444
|
+
return typeof payload.sub === 'string' && payload.sub ? payload.sub : null;
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
254
450
|
export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
255
451
|
const expiration = getJwtExpiration(jwt);
|
|
256
452
|
return expiration !== null && expiration <= nowSeconds;
|
|
@@ -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
|
+
}
|
package/src/runtime/transport.js
CHANGED
|
@@ -4,6 +4,7 @@ import { CliError, EXIT_CODES } from './errors.js';
|
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_PROFILE,
|
|
6
6
|
getProfile,
|
|
7
|
+
getJwtSubject,
|
|
7
8
|
isJwtExpired,
|
|
8
9
|
loadConfig,
|
|
9
10
|
} from './profiles.js';
|
|
@@ -122,6 +123,18 @@ function normalizeBackendError(status, payload, runtime) {
|
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
if (status === 402) {
|
|
127
|
+
// Distinct from auth on purpose: an agent that sees an auth exit code will
|
|
128
|
+
// loop trying to re-authenticate, when the actual fix is to add credit.
|
|
129
|
+
return new CliError({
|
|
130
|
+
code: 'payment_required',
|
|
131
|
+
message,
|
|
132
|
+
exitCode: EXIT_CODES.payment,
|
|
133
|
+
details: payload || {},
|
|
134
|
+
hints: payload?.hints || [],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
if (status >= 400 && status < 500) {
|
|
126
139
|
return new CliError({
|
|
127
140
|
code: 'usage_error',
|
|
@@ -149,7 +162,7 @@ function reloadJwtFromConfig(runtime) {
|
|
|
149
162
|
const profileName = runtime.profileName || DEFAULT_PROFILE;
|
|
150
163
|
let profile;
|
|
151
164
|
try {
|
|
152
|
-
profile = getProfile(loadConfig(), profileName);
|
|
165
|
+
profile = getProfile(loadConfig(runtime.worktreeRuntime), profileName);
|
|
153
166
|
} catch {
|
|
154
167
|
return false;
|
|
155
168
|
}
|
|
@@ -157,6 +170,18 @@ function reloadJwtFromConfig(runtime) {
|
|
|
157
170
|
if (!nextJwt || nextJwt === runtime.jwt) {
|
|
158
171
|
return false;
|
|
159
172
|
}
|
|
173
|
+
const expectedUserId = runtime.worktreeRuntime?.expected_user_id;
|
|
174
|
+
if (expectedUserId && getJwtSubject(nextJwt) !== expectedUserId) {
|
|
175
|
+
throw new CliError({
|
|
176
|
+
code: 'dev_runtime_identity_mismatch',
|
|
177
|
+
message: 'The refreshed scoped dev credential does not belong to this worktree test user',
|
|
178
|
+
exitCode: EXIT_CODES.auth,
|
|
179
|
+
hints: [
|
|
180
|
+
{ message: 'Restart ./dev.sh to restore the approved worktree identity.' },
|
|
181
|
+
{ message: `Expected user: ${expectedUserId}` },
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
160
185
|
runtime.jwt = nextJwt;
|
|
161
186
|
runtime.desktopAppName = profile.desktop_app_name;
|
|
162
187
|
runtime.desktopPid = profile.desktop_pid;
|
|
@@ -318,6 +343,9 @@ export async function callTool({
|
|
|
318
343
|
cwd: process.cwd(),
|
|
319
344
|
agent_mode: runtime.agentMode,
|
|
320
345
|
cli_version: runtime.cliVersion,
|
|
346
|
+
...(runtime.debugEntitlementOverride
|
|
347
|
+
? { debug_entitlement_override: runtime.debugEntitlementOverride }
|
|
348
|
+
: {}),
|
|
321
349
|
},
|
|
322
350
|
};
|
|
323
351
|
|
|
@@ -107,6 +107,33 @@
|
|
|
107
107
|
const apiBase = {{API_BASE}};
|
|
108
108
|
const jwt = {{JWT}};
|
|
109
109
|
const fixtures = {{FIXTURES}};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolves the canned response for one tool call.
|
|
113
|
+
*
|
|
114
|
+
* A plain `"TOOL_NAME"` key answers every call to that tool. An app that
|
|
115
|
+
* reaches several databases through a single tool needs to vary the
|
|
116
|
+
* payload per call, so a key may also be scoped to one argument with
|
|
117
|
+
* `"TOOL_NAME#arg=value"` — for example
|
|
118
|
+
* `"LOCAL_NOTIS_DATABASE_QUERY#database_slug=notes"`. Scoped keys are
|
|
119
|
+
* checked first; the unscoped key stays the fallback.
|
|
120
|
+
*/
|
|
121
|
+
function lookupToolFixture(name, args) {
|
|
122
|
+
const tools = fixtures && fixtures.tools;
|
|
123
|
+
if (!tools) return undefined;
|
|
124
|
+
for (const key of Object.keys(tools)) {
|
|
125
|
+
const sep = key.indexOf('#');
|
|
126
|
+
if (sep === -1 || key.slice(0, sep) !== name) continue;
|
|
127
|
+
const scope = key.slice(sep + 1);
|
|
128
|
+
const eq = scope.indexOf('=');
|
|
129
|
+
if (eq === -1) continue;
|
|
130
|
+
if (String(args[scope.slice(0, eq)]) === scope.slice(eq + 1)) {
|
|
131
|
+
return tools[key];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Object.prototype.hasOwnProperty.call(tools, name) ? tools[name] : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
110
137
|
const status = document.getElementById('harness-status');
|
|
111
138
|
const setStatus = (msg, color = '#0f0') => {
|
|
112
139
|
status.textContent = `harness: ${msg}`;
|
|
@@ -197,8 +224,9 @@
|
|
|
197
224
|
},
|
|
198
225
|
callTool: async (name, args) => {
|
|
199
226
|
record('callTool', { name, arguments: args || {} });
|
|
200
|
-
|
|
201
|
-
|
|
227
|
+
const fixture = lookupToolFixture(name, args || {});
|
|
228
|
+
if (fixture !== undefined) {
|
|
229
|
+
return structuredClone(fixture);
|
|
202
230
|
}
|
|
203
231
|
return { ok: true, result: null };
|
|
204
232
|
},
|
|
@@ -62,6 +62,20 @@ export const NOTIS_APP_CATEGORIES = [
|
|
|
62
62
|
|
|
63
63
|
export type NotisAppCategory = typeof NOTIS_APP_CATEGORIES[number];
|
|
64
64
|
|
|
65
|
+
export interface NotisAppDatabaseConfig {
|
|
66
|
+
/** Database slug, as declared by the app. */
|
|
67
|
+
slug: string;
|
|
68
|
+
/**
|
|
69
|
+
* Ship this database's rows to everyone who installs the app.
|
|
70
|
+
*
|
|
71
|
+
* Off by default: declaring a database publishes its STRUCTURE, never its
|
|
72
|
+
* content. Turn it on only for starter content that every installer should
|
|
73
|
+
* receive - a default folder tree, a set of templates - and never for a
|
|
74
|
+
* database that accumulates the author's own data.
|
|
75
|
+
*/
|
|
76
|
+
seedDocuments?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
export interface NotisAppAuthor {
|
|
66
80
|
name: string;
|
|
67
81
|
handle?: string;
|
|
@@ -106,6 +120,19 @@ export const NOTIS_APP_ACCENTS = [
|
|
|
106
120
|
|
|
107
121
|
export type NotisAppAccent = typeof NOTIS_APP_ACCENTS[number];
|
|
108
122
|
|
|
123
|
+
export interface NotisAppCapabilities {
|
|
124
|
+
/**
|
|
125
|
+
* Read every database in the workspace, not only the ones this app declares
|
|
126
|
+
* in `databases` or created itself.
|
|
127
|
+
*
|
|
128
|
+
* An app runtime is otherwise sandboxed to its own databases, so a catalog or
|
|
129
|
+
* explorer app sees an empty list without this. `'read'` is the only accepted
|
|
130
|
+
* value and it never grants writes: `LOCAL_NOTIS_DATABASE_UPSERT_*` stays
|
|
131
|
+
* bound to the app's own databases.
|
|
132
|
+
*/
|
|
133
|
+
workspaceDatabases?: 'read';
|
|
134
|
+
}
|
|
135
|
+
|
|
109
136
|
export interface NotisAppConfig {
|
|
110
137
|
/** URL-safe app slug. Existing apps may still use a display name here. */
|
|
111
138
|
name: string;
|
|
@@ -128,7 +155,17 @@ export interface NotisAppConfig {
|
|
|
128
155
|
/** @deprecated Add release entries to the root CHANGELOG.md instead. */
|
|
129
156
|
versionNotes?: string;
|
|
130
157
|
screenshots?: NotisAppScreenshotConfig[];
|
|
131
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Databases this app owns. A bare string publishes structure only; use the
|
|
160
|
+
* object form to opt a database into shipping its rows to installers.
|
|
161
|
+
*/
|
|
162
|
+
databases?: (string | NotisAppDatabaseConfig)[];
|
|
163
|
+
/**
|
|
164
|
+
* Extra permissions the app asks for at install time. Everything here widens
|
|
165
|
+
* what the app can reach beyond its own data, so each one is surfaced to the
|
|
166
|
+
* user before they install.
|
|
167
|
+
*/
|
|
168
|
+
capabilities?: NotisAppCapabilities;
|
|
132
169
|
routes?: NotisRouteConfig[];
|
|
133
170
|
tools?: string[];
|
|
134
171
|
skills?: NotisAppSkillConfig[];
|