@skrr-ai/cli 0.1.9 → 0.1.11
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/dist/base-command.d.ts +1 -13
- package/dist/base-command.js +78 -1
- package/dist/commands/browser/skill/show.js +7 -1
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +9 -1
- package/dist/commands/commitments/analytics/index.js +2 -0
- package/dist/commands/daemon/byok.d.ts +1 -0
- package/dist/commands/daemon/byok.js +2 -1
- package/dist/commands/daemon/install.d.ts +1 -0
- package/dist/commands/daemon/install.js +2 -1
- package/dist/commands/daemon/login.d.ts +31 -0
- package/dist/commands/daemon/login.js +56 -0
- package/dist/commands/daemon/restart.d.ts +9 -0
- package/dist/commands/daemon/restart.js +36 -0
- package/dist/commands/daemon/start.d.ts +1 -0
- package/dist/commands/daemon/start.js +2 -1
- package/dist/commands/daemon/status.d.ts +1 -0
- package/dist/commands/daemon/status.js +2 -1
- package/dist/commands/daemon/stop.d.ts +1 -0
- package/dist/commands/daemon/stop.js +2 -1
- package/dist/commands/daemon/uninstall.d.ts +1 -0
- package/dist/commands/daemon/uninstall.js +2 -1
- package/dist/commands/daemon/unlock.d.ts +9 -0
- package/dist/commands/daemon/unlock.js +33 -0
- package/dist/commands/goals/key-results/create.js +32 -1
- package/dist/commands/goals/key-results/update.d.ts +11 -0
- package/dist/commands/goals/key-results/update.js +80 -2
- package/dist/commands/goals/plan-now.d.ts +54 -2
- package/dist/commands/goals/plan-now.js +175 -18
- package/dist/commands/goals/planner-config.d.ts +60 -9
- package/dist/commands/goals/planner-config.js +82 -34
- package/dist/commands/goals/revisions.js +17 -0
- package/dist/commands/goals/show.d.ts +17 -0
- package/dist/commands/goals/show.js +90 -3
- package/dist/commands/login.js +36 -3
- package/dist/commands/spaces/create.js +2 -1
- package/dist/commands/spaces/index.js +9 -1
- package/dist/commands/spaces/list.d.ts +18 -0
- package/dist/commands/spaces/list.js +57 -7
- package/dist/commands/spaces/show.js +4 -1
- package/dist/commands/spaces/summary.d.ts +4 -0
- package/dist/commands/spaces/summary.js +77 -1
- package/dist/commands/spaces/update.d.ts +4 -0
- package/dist/commands/spaces/update.js +38 -1
- package/dist/commands/tasks/actionability.js +40 -1
- package/dist/commands/tasks/activity.d.ts +29 -0
- package/dist/commands/tasks/activity.js +47 -0
- package/dist/commands/tasks/complete.d.ts +47 -0
- package/dist/commands/tasks/complete.js +159 -12
- package/dist/commands/tasks/create.d.ts +26 -0
- package/dist/commands/tasks/create.js +60 -1
- package/dist/commands/tasks/events/append.d.ts +2 -0
- package/dist/commands/tasks/events/append.js +41 -10
- package/dist/commands/tasks/events/list.js +60 -9
- package/dist/commands/tasks/output.js +22 -2
- package/dist/commands/tasks/ready.d.ts +38 -0
- package/dist/commands/tasks/ready.js +37 -0
- package/dist/commands/tasks/runs.d.ts +22 -0
- package/dist/commands/tasks/runs.js +110 -2
- package/dist/commands/tasks/show.d.ts +28 -0
- package/dist/commands/tasks/show.js +61 -0
- package/dist/commands/tasks/timeline.d.ts +7 -0
- package/dist/commands/tasks/timeline.js +34 -3
- package/dist/commands/tasks/update.d.ts +20 -0
- package/dist/commands/tasks/update.js +38 -1
- package/dist/commands/whoami.d.ts +40 -0
- package/dist/commands/whoami.js +61 -10
- package/dist/commands/wiki/ls.d.ts +23 -0
- package/dist/commands/wiki/ls.js +63 -10
- package/dist/commands/wiki/mv.d.ts +70 -0
- package/dist/commands/wiki/mv.js +198 -4
- package/dist/commands/wiki/rm.js +12 -1
- package/dist/commands/wiki/write.js +35 -3
- package/dist/help.d.ts +27 -0
- package/dist/help.js +48 -0
- package/dist/hooks/command-not-found.d.ts +31 -0
- package/dist/hooks/command-not-found.js +12 -80
- package/dist/lib/command-miss.d.ts +60 -0
- package/dist/lib/command-miss.js +128 -0
- package/dist/lib/commitment-analytics.d.ts +10 -0
- package/dist/lib/commitment-analytics.js +10 -0
- package/dist/lib/config.d.ts +40 -0
- package/dist/lib/config.js +72 -9
- package/dist/lib/daemonBroker.d.ts +7 -1
- package/dist/lib/daemonBroker.js +71 -0
- package/dist/lib/daemonHandoff.d.ts +38 -0
- package/dist/lib/daemonHandoff.js +297 -0
- package/dist/lib/exec-oversky.d.ts +30 -0
- package/dist/lib/exec-oversky.js +41 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +27 -5
- package/dist/lib/login.js +22 -2
- package/dist/lib/sky-code.js +9 -4
- package/dist/lib/task-transcript.d.ts +9 -0
- package/dist/lib/task-transcript.js +13 -2
- package/dist/lib/tasks.d.ts +34 -0
- package/dist/lib/tasks.js +70 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
- package/oclif.manifest.json +15713 -15480
- package/package.json +5 -3
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handOffToLocalDaemon = handOffToLocalDaemon;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const exec_oversky_1 = require("./exec-oversky");
|
|
8
|
+
const keychain_1 = require("./keychain");
|
|
9
|
+
/**
|
|
10
|
+
* Ceilings, not expectations. Both calls finish in milliseconds when anything is
|
|
11
|
+
* working; these exist so that when something is NOT working the login ends.
|
|
12
|
+
*
|
|
13
|
+
* The failure this prevents is the worst one available here: `skrr login` has
|
|
14
|
+
* already printed "Login successful", and an unbounded wait on a child process
|
|
15
|
+
* leaves the user staring at a terminal that will never return, with no way to
|
|
16
|
+
* tell a hung handoff from a slow one. A blocked keychain prompt, a daemon
|
|
17
|
+
* binary wedged on a lock, an NFS home directory — none of them are exotic, and
|
|
18
|
+
* all of them used to hang the command.
|
|
19
|
+
*
|
|
20
|
+
* The accept side gets the longer budget because it may have to unlock a
|
|
21
|
+
* keychain. Killing it mid-write is the cost, and it is the lesser one: the
|
|
22
|
+
* credential write is a few milliseconds at the end of that window, so a timeout
|
|
23
|
+
* overwhelmingly lands BEFORE it rather than during it, and `skrr daemon login`
|
|
24
|
+
* recovers either way.
|
|
25
|
+
*/
|
|
26
|
+
const PROBE_TIMEOUT_MS = 15_000;
|
|
27
|
+
const ACCEPT_TIMEOUT_MS = 60_000;
|
|
28
|
+
function runOversky(binary, args, opts) {
|
|
29
|
+
const { input, timeoutMs } = opts;
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
// A Windows npm-global install ships a `.cmd` shim, which `spawn` cannot
|
|
32
|
+
// execute without a shell — Node fires ENOENT instead. `execOversky` has
|
|
33
|
+
// handled this since it was written; this function did not, so the handoff
|
|
34
|
+
// failed on EVERY Windows login and reported it as the daemon refusing the
|
|
35
|
+
// bundle. Restricted to the shim case so real binaries keep the argv-array
|
|
36
|
+
// exec path, which needs no quoting rules to be right.
|
|
37
|
+
const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(binary);
|
|
38
|
+
const child = (0, node_child_process_1.spawn)(binary, args, {
|
|
39
|
+
// stderr is CAPTURED, not discarded. Discarding it cost a debugging cycle
|
|
40
|
+
// on the first real run: the daemon exited non-zero with
|
|
41
|
+
// `OVERSKY_KEK_REQUIRED`, and all the user saw was "the daemon refused the
|
|
42
|
+
// handoff" — an outcome with its reason thrown away. A wrapper that hides
|
|
43
|
+
// the wrapped program's error is worse than one that does not wrap.
|
|
44
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
45
|
+
...(isWinShim ? { shell: true } : {}),
|
|
46
|
+
});
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let settled = false;
|
|
50
|
+
const settle = (value) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve(value);
|
|
56
|
+
};
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
child.kill('SIGKILL');
|
|
59
|
+
settle({
|
|
60
|
+
ok: false,
|
|
61
|
+
stdout,
|
|
62
|
+
stderr: `timed out after ${Math.round(timeoutMs / 1000)}s`,
|
|
63
|
+
});
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
// Never hold the event loop open on the timer alone.
|
|
66
|
+
if (typeof timer.unref === 'function')
|
|
67
|
+
timer.unref();
|
|
68
|
+
child.stdout?.on('data', (c) => (stdout += String(c)));
|
|
69
|
+
child.stderr?.on('data', (c) => (stderr += String(c)));
|
|
70
|
+
child.on('error', (err) => settle({ ok: false, stdout, stderr: err.message }));
|
|
71
|
+
child.on('close', (code) => settle({ ok: code === 0, stdout, stderr }));
|
|
72
|
+
if (input !== undefined) {
|
|
73
|
+
// An EPIPE here is the child having already exited — its own exit code and
|
|
74
|
+
// stderr are the real diagnosis, and an unhandled stream error would
|
|
75
|
+
// instead crash the CLI one line after a successful login.
|
|
76
|
+
child.stdin?.on('error', () => {
|
|
77
|
+
/* reported by the close handler */
|
|
78
|
+
});
|
|
79
|
+
child.stdin?.end(input);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The profile flags to pass to `skrrd`, so both spawns land in the same tenancy
|
|
85
|
+
* the CLI is signed in to.
|
|
86
|
+
*
|
|
87
|
+
* A profile is the daemon's tenancy boundary — separate config, state, keychain
|
|
88
|
+
* namespace and service label. The CLI resolves its own; the daemon resolves its
|
|
89
|
+
* own, from `--profile` → `OVERSKY_PROFILE` → its `profile.lock` → `default`.
|
|
90
|
+
* Passing nothing let those two answers differ, and the failure was silent in the
|
|
91
|
+
* damaging direction: `skrr --profile work login` would mint against the id
|
|
92
|
+
* printed by whichever profile the daemon's lock happened to name, then write a
|
|
93
|
+
* work-profile credential over THAT profile's store.
|
|
94
|
+
*
|
|
95
|
+
* `skrr daemon login` already binds the profile this way (`bindToCliSession`);
|
|
96
|
+
* this is the same rule for the path that runs without being asked.
|
|
97
|
+
*/
|
|
98
|
+
function profileArgs() {
|
|
99
|
+
const profile = (0, keychain_1.getActiveProfile)();
|
|
100
|
+
return profile && profile !== keychain_1.DEFAULT_PROFILE ? ['--profile', profile] : [];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Parse the machine-readable line `accept-handoff --print-id` writes to stderr.
|
|
104
|
+
*
|
|
105
|
+
* Absent on a daemon that predates it, which is a normal state during a rollout
|
|
106
|
+
* and not an error — the caller degrades to the older, less careful behaviour
|
|
107
|
+
* rather than refusing.
|
|
108
|
+
*/
|
|
109
|
+
function parseIdProvenance(stderr) {
|
|
110
|
+
const line = stderr.split('\n').find((l) => l.includes('skrrd:accept-handoff'));
|
|
111
|
+
if (!line)
|
|
112
|
+
return {};
|
|
113
|
+
const idSource = /id-source=([A-Za-z0-9._:-]+)/.exec(line)?.[1];
|
|
114
|
+
const machineUuid = /machine-uuid=([A-Za-z0-9._:-]+)/.exec(line)?.[1];
|
|
115
|
+
// Any non-space run: this one is a URL, and a URL contains characters the
|
|
116
|
+
// other two never do.
|
|
117
|
+
const daemonServer = /server=(\S+)/.exec(line)?.[1];
|
|
118
|
+
return { idSource, machineUuid, daemonServer };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What actually changed on this machine, said out loud.
|
|
122
|
+
*
|
|
123
|
+
* A handoff can re-point the machine to a different ACCOUNT (you signed in as
|
|
124
|
+
* someone else) or a different DEPLOYMENT (your daemon was on dev, your CLI is
|
|
125
|
+
* on prod). Both are the feature working — the machine follows the human, and
|
|
126
|
+
* moving a dev-bound daemon to prod is the exact story this path was written for
|
|
127
|
+
* — and both are state changes an operator should watch happen rather than
|
|
128
|
+
* discover later, when a daemon they thought was on dev answers from prod.
|
|
129
|
+
*
|
|
130
|
+
* Degrades cleanly: an older server sends no account, an older daemon reports no
|
|
131
|
+
* server, and the sentence simply says less rather than guessing.
|
|
132
|
+
*/
|
|
133
|
+
function describeDelivery(opts) {
|
|
134
|
+
const parts = [];
|
|
135
|
+
parts.push(opts.accountLabel
|
|
136
|
+
? `now signed in as ${opts.accountLabel} on ${(0, config_1.describeDeployment)(opts.cliBaseURL)}`
|
|
137
|
+
: 'the local daemon now has its own credential');
|
|
138
|
+
if (opts.daemonServer &&
|
|
139
|
+
canonicalServer(opts.daemonServer) !== canonicalServer(opts.cliBaseURL)) {
|
|
140
|
+
parts.push(`moved from ${(0, config_1.describeDeployment)(opts.daemonServer)}`);
|
|
141
|
+
}
|
|
142
|
+
return parts.join(', ');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Compare deployments, not spellings.
|
|
146
|
+
*
|
|
147
|
+
* `https://oversky.ai` and `https://skrr.ai` are one deployment under two names,
|
|
148
|
+
* and the first is still what every daemon installed before the cutover carries.
|
|
149
|
+
* Trimming and lowercasing alone would report a move on every login from every
|
|
150
|
+
* one of those machines — a warning that is wrong for the largest group of users
|
|
151
|
+
* who would see it, which is how a signal becomes noise. `canonicalizeKnownBaseURL`
|
|
152
|
+
* is the fold the rest of the CLI already compares through.
|
|
153
|
+
*/
|
|
154
|
+
function canonicalServer(url) {
|
|
155
|
+
return (0, config_1.canonicalizeKnownBaseURL)(url).trim().replace(/\/+$/, '').toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
/** The most useful line of a failure, so a remedy can name the cause. */
|
|
158
|
+
function firstMeaningfulLine(text) {
|
|
159
|
+
const line = text
|
|
160
|
+
.split('\n')
|
|
161
|
+
.map((l) => l.trim())
|
|
162
|
+
.find((l) => l &&
|
|
163
|
+
!l.startsWith('at ') &&
|
|
164
|
+
!/^[{}]/.test(l) &&
|
|
165
|
+
// The provenance line is a protocol between these two programs, not a
|
|
166
|
+
// diagnosis. Reporting it as one would answer "why did this fail?" with
|
|
167
|
+
// a machine's own bookkeeping.
|
|
168
|
+
!l.includes('skrrd:accept-handoff'));
|
|
169
|
+
return line ? line.slice(0, 160) : 'no output';
|
|
170
|
+
}
|
|
171
|
+
async function handOffToLocalDaemon(env = process.env, opts = {}) {
|
|
172
|
+
// Mirrors `OVERSKY_SKIP_DAEMON_BROKER` on the other direction. An operator
|
|
173
|
+
// deliberately running a daemon against a different deployment than their CLI is
|
|
174
|
+
// doing a legitimate thing, and this must not overrule it.
|
|
175
|
+
if (env.OVERSKY_SKIP_DAEMON_HANDOFF === '1' || env.SKRR_SKIP_DAEMON_HANDOFF === '1') {
|
|
176
|
+
return { status: 'skipped', detail: 'skipped by OVERSKY_SKIP_DAEMON_HANDOFF' };
|
|
177
|
+
}
|
|
178
|
+
// The daemon brokered this login, which means it just used its OWN credential
|
|
179
|
+
// against this server and that credential works. There is nothing to fix.
|
|
180
|
+
//
|
|
181
|
+
// Running anyway is not merely redundant, it is destructive: it mints a
|
|
182
|
+
// replacement, writes it over the working one, revokes the family the daemon is
|
|
183
|
+
// authenticated with, and restarts the process — killing whatever sessions it
|
|
184
|
+
// was running. `skrr login` on a healthy machine is the path where the least
|
|
185
|
+
// should happen, and it was the path where the most did.
|
|
186
|
+
if (opts.loginFlow === 'daemon-broker') {
|
|
187
|
+
return {
|
|
188
|
+
status: 'skipped',
|
|
189
|
+
detail: 'the local daemon brokered this login, so it already has a working credential',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const binary = (0, exec_oversky_1.findOverskyBinary)();
|
|
193
|
+
if (!binary) {
|
|
194
|
+
return { status: 'skipped', detail: 'no local daemon installed' };
|
|
195
|
+
}
|
|
196
|
+
// An older daemon has no `accept-handoff`, and spawning it would fail with
|
|
197
|
+
// commander's "unknown command" rather than anything a reader could act on. A
|
|
198
|
+
// CLI that outruns the daemon on the machine is the normal case during a
|
|
199
|
+
// staged rollout, not an error.
|
|
200
|
+
const supported = await (0, exec_oversky_1.overskySupportsSubcommand)('accept-handoff');
|
|
201
|
+
if (supported === false) {
|
|
202
|
+
return {
|
|
203
|
+
status: 'skipped',
|
|
204
|
+
detail: 'the installed daemon predates `accept-handoff`; run `skrr daemon login` once',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Ask the daemon which id it will bind, rather than deriving it here. The id
|
|
208
|
+
// comes from cwd + profile, and a second copy of that derivation in the CLI
|
|
209
|
+
// would drift — producing a credential minted for a daemon that does not exist,
|
|
210
|
+
// which is indistinguishable from a login that simply did not work.
|
|
211
|
+
const printed = await runOversky(binary, ['accept-handoff', '--print-id', ...profileArgs()], {
|
|
212
|
+
timeoutMs: PROBE_TIMEOUT_MS,
|
|
213
|
+
});
|
|
214
|
+
const daemonId = printed.ok ? printed.stdout.trim() : '';
|
|
215
|
+
if (!daemonId) {
|
|
216
|
+
return {
|
|
217
|
+
status: 'failed',
|
|
218
|
+
detail: `could not read the local daemon id — ${firstMeaningfulLine(printed.stderr)}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const { idSource, machineUuid, daemonServer } = parseIdProvenance(printed.stderr);
|
|
222
|
+
// A well-formed id is not the same as a real daemon. With no service installed
|
|
223
|
+
// the daemon derives one from whatever directory this process is in, so the id
|
|
224
|
+
// above names a daemon that does not exist and will not exist under that id if
|
|
225
|
+
// one is ever installed from a different directory. Minting for it leaves an
|
|
226
|
+
// orphaned daemon-scope family on the account and hands the credential to
|
|
227
|
+
// nothing — which is the case the design record says to skip, and which the
|
|
228
|
+
// binary-presence check alone never detected because a binary on PATH is not a
|
|
229
|
+
// service.
|
|
230
|
+
//
|
|
231
|
+
// Only a daemon that reports its provenance can be skipped this way. An older
|
|
232
|
+
// one says nothing, and is left on the previous behaviour rather than refused.
|
|
233
|
+
if (idSource === 'cwd') {
|
|
234
|
+
return {
|
|
235
|
+
status: 'skipped',
|
|
236
|
+
detail: 'no daemon service is installed on this machine',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let bundle;
|
|
240
|
+
try {
|
|
241
|
+
bundle = (await data_provider_1.dataService.mintDaemonHandoff({
|
|
242
|
+
daemonId,
|
|
243
|
+
...(machineUuid ? { machineUuid } : {}),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { status: 'failed', detail: err.message };
|
|
248
|
+
}
|
|
249
|
+
const payload = JSON.stringify({
|
|
250
|
+
refreshToken: bundle.refreshToken,
|
|
251
|
+
refreshExpiresAt: bundle.refreshExpiresAt,
|
|
252
|
+
serverUrl: (0, config_1.loadConfig)().baseURL,
|
|
253
|
+
daemonId,
|
|
254
|
+
});
|
|
255
|
+
// No restart from here. `accept-handoff` runs the runtime's own
|
|
256
|
+
// `restartDisconnectedServiceAfterLogin` after it writes the credential — the
|
|
257
|
+
// same reconcile every other credential-write path in the daemon uses — and it
|
|
258
|
+
// is the better of the two: it checks that a service is installed and running,
|
|
259
|
+
// refuses to yank a daemon another profile owns, and gives the in-process
|
|
260
|
+
// watcher a window to converge before reaching for the heavy hammer.
|
|
261
|
+
//
|
|
262
|
+
// A second restart out here does not add a guarantee, it adds an outage. Each
|
|
263
|
+
// one kills in-flight Claude/Codex sessions and browser CDP attachments, and
|
|
264
|
+
// the second would land on a process that is seconds into starting up.
|
|
265
|
+
const accepted = await runOversky(binary, ['accept-handoff', ...profileArgs()], {
|
|
266
|
+
input: payload,
|
|
267
|
+
timeoutMs: ACCEPT_TIMEOUT_MS,
|
|
268
|
+
});
|
|
269
|
+
if (!accepted.ok) {
|
|
270
|
+
// Deliberately NOT confirming. The machine still holds the credential it had
|
|
271
|
+
// before this ran, and it still works — the mint no longer revokes anything,
|
|
272
|
+
// so the cost of a refused handoff is one unused family that the next
|
|
273
|
+
// successful login sweeps, rather than a signed-out machine.
|
|
274
|
+
return { status: 'failed', detail: firstMeaningfulLine(accepted.stderr) };
|
|
275
|
+
}
|
|
276
|
+
// Only now is the predecessor retired: the daemon has written the new
|
|
277
|
+
// credential to its own store, so "the old one dies iff the new one is in
|
|
278
|
+
// place" holds. Best-effort — a confirm that does not land leaves one extra
|
|
279
|
+
// live family, bounded by the server's concurrent-session cap and swept by the
|
|
280
|
+
// next confirm, which is a far cheaper failure than the one it replaces.
|
|
281
|
+
if (bundle.familyId) {
|
|
282
|
+
try {
|
|
283
|
+
await data_provider_1.dataService.confirmDaemonHandoff({ daemonId, familyId: bundle.familyId });
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
/* the credential is delivered; the sweep is bookkeeping */
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
status: 'delivered',
|
|
291
|
+
detail: describeDelivery({
|
|
292
|
+
accountLabel: bundle.accountLabel,
|
|
293
|
+
daemonServer,
|
|
294
|
+
cliBaseURL: (0, config_1.loadConfig)().baseURL,
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
/** Runtime binary names, current first. */
|
|
2
2
|
export declare const RUNTIME_BINARY_NAMES: readonly ["skrrd", "oversky"];
|
|
3
|
+
/**
|
|
4
|
+
* The sentence a `skrr daemon` pass-through appends to its description.
|
|
5
|
+
*
|
|
6
|
+
* OSK-296 made these commands forward their flags to `skrrd` instead of
|
|
7
|
+
* rejecting them, and stopped there. oclif renders the FLAGS section of help
|
|
8
|
+
* from a command's declared flags, and a pass-through declares none — so help
|
|
9
|
+
* kept describing the old, broken behaviour where these commands took nothing,
|
|
10
|
+
* while `skrr daemon status --profile work` quietly worked. The entire flag
|
|
11
|
+
* surface of the wrapped binary became reachable and undiscoverable in the
|
|
12
|
+
* same commit.
|
|
13
|
+
*
|
|
14
|
+
* `skrr code` hit this first and fixed BOTH halves: its description says out
|
|
15
|
+
* loud that engine flags are not listed there and where to find them. This is
|
|
16
|
+
* that sentence, for the other wrapper, in one place so eight commands cannot
|
|
17
|
+
* drift into eight wordings.
|
|
18
|
+
*
|
|
19
|
+
* It names `skrrd --help` separately because `--profile` is a GLOBAL option on
|
|
20
|
+
* the runtime, so a reader sent only to the subcommand's help would not find
|
|
21
|
+
* the flag they are most likely to want.
|
|
22
|
+
*
|
|
23
|
+
* This is a command's `description`, and each command keeps a short `summary`
|
|
24
|
+
* beside it. oclif renders `summary` in the topic listing and `description`
|
|
25
|
+
* only in the command's own help, so the note reaches someone asking about
|
|
26
|
+
* THIS command without turning `skrr daemon --help` into eight paragraphs of
|
|
27
|
+
* the same boilerplate — which is what it did when this text was appended to
|
|
28
|
+
* `description` alone.
|
|
29
|
+
*/
|
|
30
|
+
export declare function forwardedFlagsNote(verb: string, opts?: {
|
|
31
|
+
binds?: boolean;
|
|
32
|
+
}): string;
|
|
3
33
|
/** Resolve the absolute path of `oversky` if it exists, else return null. */
|
|
4
34
|
export declare function findOverskyBinary(): string | null;
|
|
5
35
|
/**
|
package/dist/lib/exec-oversky.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.RUNTIME_BINARY_NAMES = void 0;
|
|
7
|
+
exports.forwardedFlagsNote = forwardedFlagsNote;
|
|
7
8
|
exports.findOverskyBinary = findOverskyBinary;
|
|
8
9
|
exports.notInstalledMessage = notInstalledMessage;
|
|
9
10
|
exports.execOversky = execOversky;
|
|
@@ -31,6 +32,46 @@ const node_fs_1 = require("node:fs");
|
|
|
31
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
32
33
|
/** Runtime binary names, current first. */
|
|
33
34
|
exports.RUNTIME_BINARY_NAMES = ['skrrd', 'oversky'];
|
|
35
|
+
/**
|
|
36
|
+
* The sentence a `skrr daemon` pass-through appends to its description.
|
|
37
|
+
*
|
|
38
|
+
* OSK-296 made these commands forward their flags to `skrrd` instead of
|
|
39
|
+
* rejecting them, and stopped there. oclif renders the FLAGS section of help
|
|
40
|
+
* from a command's declared flags, and a pass-through declares none — so help
|
|
41
|
+
* kept describing the old, broken behaviour where these commands took nothing,
|
|
42
|
+
* while `skrr daemon status --profile work` quietly worked. The entire flag
|
|
43
|
+
* surface of the wrapped binary became reachable and undiscoverable in the
|
|
44
|
+
* same commit.
|
|
45
|
+
*
|
|
46
|
+
* `skrr code` hit this first and fixed BOTH halves: its description says out
|
|
47
|
+
* loud that engine flags are not listed there and where to find them. This is
|
|
48
|
+
* that sentence, for the other wrapper, in one place so eight commands cannot
|
|
49
|
+
* drift into eight wordings.
|
|
50
|
+
*
|
|
51
|
+
* It names `skrrd --help` separately because `--profile` is a GLOBAL option on
|
|
52
|
+
* the runtime, so a reader sent only to the subcommand's help would not find
|
|
53
|
+
* the flag they are most likely to want.
|
|
54
|
+
*
|
|
55
|
+
* This is a command's `description`, and each command keeps a short `summary`
|
|
56
|
+
* beside it. oclif renders `summary` in the topic listing and `description`
|
|
57
|
+
* only in the command's own help, so the note reaches someone asking about
|
|
58
|
+
* THIS command without turning `skrr daemon --help` into eight paragraphs of
|
|
59
|
+
* the same boilerplate — which is what it did when this text was appended to
|
|
60
|
+
* `description` alone.
|
|
61
|
+
*/
|
|
62
|
+
function forwardedFlagsNote(verb, opts = {}) {
|
|
63
|
+
const base = `Flags are forwarded to \`skrrd ${verb}\` unchanged, so they are not listed here —` +
|
|
64
|
+
` run \`skrrd ${verb} --help\` for this verb's own flags, or \`skrrd --help\` for` +
|
|
65
|
+
' global ones such as `--profile`.';
|
|
66
|
+
// `install` and `login` are not pure pass-throughs: they fill in the server
|
|
67
|
+
// and profile from the CLI's own session when the caller named neither, so a
|
|
68
|
+
// daemon cannot silently end up on a different deployment than the CLI that
|
|
69
|
+
// installed it (OSK-279). Saying so is the difference between a flag you may
|
|
70
|
+
// pass and a flag that is already being passed for you.
|
|
71
|
+
return opts.binds
|
|
72
|
+
? `${base} \`--server\` and \`--profile\` are filled in from your CLI session unless you pass them.`
|
|
73
|
+
: base;
|
|
74
|
+
}
|
|
34
75
|
/** Extra well-known install locations to check when PATH lookup misses. */
|
|
35
76
|
const FALLBACK_PATHS = exports.RUNTIME_BINARY_NAMES.flatMap((name) => [
|
|
36
77
|
`/opt/homebrew/bin/${name}`,
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export declare function truncate(s: string, max: number): string;
|
|
|
34
34
|
* reads as missing instead of as the string `undefined`.
|
|
35
35
|
*/
|
|
36
36
|
export declare function formatCell(value: unknown, placeholder?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Collapse newlines/runs of whitespace so a value cannot break the table grid.
|
|
39
|
+
*
|
|
40
|
+
* Exported because `renderTable` applies it to EVERY cell, not just the ones
|
|
41
|
+
* that happened to come through `formatCell`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function collapseWhitespace(value: string): string;
|
|
37
44
|
/**
|
|
38
45
|
* Render an array of row objects as a column-aligned table.
|
|
39
46
|
*
|
package/dist/lib/format.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.displayWidth = displayWidth;
|
|
|
4
4
|
exports.pad = pad;
|
|
5
5
|
exports.truncate = truncate;
|
|
6
6
|
exports.formatCell = formatCell;
|
|
7
|
+
exports.collapseWhitespace = collapseWhitespace;
|
|
7
8
|
exports.renderTable = renderTable;
|
|
8
9
|
exports.localStamp = localStamp;
|
|
9
10
|
exports.localZone = localZone;
|
|
@@ -141,28 +142,49 @@ function stringifyCell(value, depth) {
|
|
|
141
142
|
.filter(Boolean)
|
|
142
143
|
.join(' ');
|
|
143
144
|
}
|
|
144
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Collapse newlines/runs of whitespace so a value cannot break the table grid.
|
|
147
|
+
*
|
|
148
|
+
* Exported because `renderTable` applies it to EVERY cell, not just the ones
|
|
149
|
+
* that happened to come through `formatCell`.
|
|
150
|
+
*/
|
|
145
151
|
function collapseWhitespace(value) {
|
|
146
152
|
return value.replace(/\s+/g, ' ').trim();
|
|
147
153
|
}
|
|
148
154
|
function renderTable(rows, columns, log) {
|
|
149
155
|
if (rows.length === 0)
|
|
150
156
|
return;
|
|
157
|
+
// Every cell is collapsed ONCE, here, before anything measures it. A
|
|
158
|
+
// fixed-width cell cannot legally contain a newline, and the table is the
|
|
159
|
+
// only place that knows that — a caller passing `String(item.preview)` has
|
|
160
|
+
// no idea whether the API's preview is one line or twelve.
|
|
161
|
+
//
|
|
162
|
+
// It has to happen before the width pass, not just at render: `displayWidth`
|
|
163
|
+
// counts a newline as one column, so an embedded one made the measurement
|
|
164
|
+
// wrong as well as the output. `tasks complete` writes a multi-line
|
|
165
|
+
// `---TASK_OUTPUT---` comment, so `tasks timeline` exploded one row across
|
|
166
|
+
// three physical lines and lost column alignment for everything after it —
|
|
167
|
+
// on every task that had been completed, which is all of them.
|
|
168
|
+
//
|
|
169
|
+
// `verbatim` is exempt from CLIPPING, not from this: an unclipped newline
|
|
170
|
+
// breaks the grid exactly as badly, and an id column never contains one.
|
|
171
|
+
const cells = rows.map((row) => Object.fromEntries(columns.map((col) => [String(col.key), collapseWhitespace(String(row[col.key] ?? ''))])));
|
|
151
172
|
const widths = {};
|
|
152
173
|
for (const col of columns) {
|
|
153
174
|
const key = String(col.key);
|
|
154
175
|
const headerWidth = displayWidth(col.header);
|
|
155
|
-
const dataWidth = Math.max(...
|
|
176
|
+
const dataWidth = Math.max(0, ...cells.map((r) => displayWidth(r[key])));
|
|
156
177
|
const natural = Math.max(headerWidth, dataWidth);
|
|
157
178
|
widths[key] = col.maxWidth && !col.verbatim ? Math.min(natural, col.maxWidth) : natural;
|
|
158
179
|
}
|
|
159
180
|
const header = columns.map((col) => pad(col.header, widths[String(col.key)])).join(' ');
|
|
160
181
|
log(header);
|
|
161
|
-
for (const row of
|
|
182
|
+
for (const row of cells) {
|
|
162
183
|
const line = columns
|
|
163
184
|
.map((col) => {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
185
|
+
const key = String(col.key);
|
|
186
|
+
const raw = row[key];
|
|
187
|
+
const width = widths[key];
|
|
166
188
|
return pad(col.verbatim ? raw : truncate(raw, width), width);
|
|
167
189
|
})
|
|
168
190
|
.join(' ');
|
package/dist/lib/login.js
CHANGED
|
@@ -52,6 +52,8 @@ async function cliLogin(opts = {}) {
|
|
|
52
52
|
}
|
|
53
53
|
const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
|
|
54
54
|
const ssh = (0, ssh_detect_1.detectSshSession)();
|
|
55
|
+
/** Set when the broker was refused for a reason that WAITING fixes. */
|
|
56
|
+
let brokerRateLimited = false;
|
|
55
57
|
// --- Daemon-as-broker pre-flight ---------------------------------------
|
|
56
58
|
// When the local `oversky` daemon is already authenticated for this user
|
|
57
59
|
// on this machine, the cleanest UX is no second browser flow at all:
|
|
@@ -114,6 +116,20 @@ async function cliLogin(opts = {}) {
|
|
|
114
116
|
// the PKCE flow opens a browser to a different URL than the
|
|
115
117
|
// daemon. Continues to PKCE below — that's the right fallback for
|
|
116
118
|
// "I'm logging into a different server than the daemon".
|
|
119
|
+
// Transient by construction — the ceiling is per 15-minute window — so the
|
|
120
|
+
// useful thing to say is "wait", not "here is a browser". An interactive
|
|
121
|
+
// user still gets the browser, because that genuinely works for them; the
|
|
122
|
+
// note exists so a surprise browser has a stated cause. Non-interactive
|
|
123
|
+
// callers get the real reason from the throw below instead of a sentence
|
|
124
|
+
// about terminals.
|
|
125
|
+
if (brokerResult.reason === 'rate_limited') {
|
|
126
|
+
brokerRateLimited = true;
|
|
127
|
+
if (!(0, ssh_detect_1.isNonInteractive)()) {
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' The local skrr runtime has brokered several logins recently and the server is ' +
|
|
130
|
+
'rate-limiting them. Falling back to the browser flow.');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
117
133
|
if (brokerResult.reason === 'base_url_mismatch' && brokerResult.daemonServerUrl) {
|
|
118
134
|
console.log('');
|
|
119
135
|
console.log(` The local skrr runtime is logged in to ${brokerResult.daemonServerUrl}, but skrr is configured for ${serverUrl}.`);
|
|
@@ -121,8 +137,12 @@ async function cliLogin(opts = {}) {
|
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
139
|
if ((0, ssh_detect_1.isNonInteractive)()) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
'
|
|
140
|
+
throw new Error(brokerRateLimited
|
|
141
|
+
? 'The local skrr runtime is being rate-limited while brokering logins, and this ' +
|
|
142
|
+
'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
|
|
143
|
+
'retry shortly. For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...'
|
|
144
|
+
: 'skrr login requires an interactive terminal or an SSH session. ' +
|
|
145
|
+
'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
|
|
126
146
|
}
|
|
127
147
|
// --- Decision tree (first match wins) ---
|
|
128
148
|
if (opts.forceDeviceCode) {
|
package/dist/lib/sky-code.js
CHANGED
|
@@ -35,6 +35,7 @@ const node_fs_1 = require("node:fs");
|
|
|
35
35
|
const node_os_1 = require("node:os");
|
|
36
36
|
const node_path_1 = __importDefault(require("node:path"));
|
|
37
37
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
38
|
+
const config_1 = require("./config");
|
|
38
39
|
// Deliberately does NOT import the managed marker any more. This module used to
|
|
39
40
|
// read it to decide whether a run was managed; that decision now arrives as a
|
|
40
41
|
// declared `credentialMode`, and re-introducing the import here would be the first
|
|
@@ -51,14 +52,18 @@ exports.ENGINE_PATH_ENV = 'OVERSKY_SKY_CODE_PATH';
|
|
|
51
52
|
* ordering below exists to prevent.
|
|
52
53
|
*/
|
|
53
54
|
function engineHome(env = process.env) {
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// `configRoot` and not a second reading of the environment: deriving the root
|
|
56
|
+
// twice is what let this function and `getConfigPath()` disagree (OSK-300).
|
|
57
|
+
return node_path_1.default.join((0, config_1.configRoot)(env), 'sky-code');
|
|
56
58
|
}
|
|
57
59
|
/** The user-level instruction root owned by skrr Code. */
|
|
58
60
|
function skyCodeInstructionHome(env = process.env) {
|
|
59
61
|
const configured = env.SKY_CODE_HOME?.trim();
|
|
62
|
+
// Falls through to `engineHome()` rather than re-deriving `~/.skrr` — the two
|
|
63
|
+
// describe the same root, and re-deriving it is what made them disagree under
|
|
64
|
+
// `OVERSKY_CONFIG_DIR` (OSK-300).
|
|
60
65
|
if (!configured)
|
|
61
|
-
return
|
|
66
|
+
return engineHome(env);
|
|
62
67
|
if (configured === '~')
|
|
63
68
|
return (0, node_os_1.homedir)();
|
|
64
69
|
if (configured.startsWith('~/'))
|
|
@@ -86,7 +91,7 @@ function managedEnginePath(exeName, env = process.env) {
|
|
|
86
91
|
* hard-coded home would defeat the pinning {@link engineHome} relies on.
|
|
87
92
|
*/
|
|
88
93
|
function legacyManagedEnginePath(exeName, env = process.env) {
|
|
89
|
-
if (
|
|
94
|
+
if ((0, auth_core_1.isConfigRootOverridden)(env))
|
|
90
95
|
return null;
|
|
91
96
|
return node_path_1.default.join((0, node_os_1.homedir)(), '.oversky', 'sky-code', 'bin', exeName);
|
|
92
97
|
}
|
|
@@ -23,6 +23,15 @@ export type TaskOutputComment = {
|
|
|
23
23
|
parsed: ParsedTaskOutput;
|
|
24
24
|
/** Block lines the field parser could not attribute — see `unattributedBlockLines`. */
|
|
25
25
|
unattributed: string[];
|
|
26
|
+
/**
|
|
27
|
+
* How many OLDER TASK_OUTPUT blocks this one supersedes.
|
|
28
|
+
*
|
|
29
|
+
* `tasks complete` can run twice on an already-done task, and each run posts
|
|
30
|
+
* its own block. Callers render the newest, so without this count a screen
|
|
31
|
+
* built from several completions is indistinguishable from one built from a
|
|
32
|
+
* single completion.
|
|
33
|
+
*/
|
|
34
|
+
supersededCount: number;
|
|
26
35
|
};
|
|
27
36
|
/**
|
|
28
37
|
* Mirror of `api/server/services/taskCompletionFormatting.js:parseTaskOutputBlock`.
|
|
@@ -130,21 +130,32 @@ async function fetchLatestTaskOutputComment(taskId, limit = 50) {
|
|
|
130
130
|
const comments = response?.data ?? [];
|
|
131
131
|
// listTaskComments returns newest-first; take the first parseable block so a
|
|
132
132
|
// later "action items tracked" follow-up comment doesn't mask the real output.
|
|
133
|
+
// Keep walking after finding it, though — the REST are earlier completions,
|
|
134
|
+
// and their number is the only signal a caller has that the newest block is
|
|
135
|
+
// not the whole story.
|
|
136
|
+
let newest = null;
|
|
137
|
+
let blocks = 0;
|
|
133
138
|
for (const comment of comments) {
|
|
134
139
|
const body = String(comment.body ?? comment.content ?? '');
|
|
135
140
|
const parsed = parseTaskOutputBlock(body);
|
|
136
141
|
if (!parsed)
|
|
137
142
|
continue;
|
|
138
|
-
|
|
143
|
+
blocks += 1;
|
|
144
|
+
if (newest)
|
|
145
|
+
continue;
|
|
146
|
+
newest = {
|
|
139
147
|
commentId: String(comment.id ?? comment._id ?? '-'),
|
|
140
148
|
createdAt: comment.createdAt ? String(comment.createdAt) : null,
|
|
141
149
|
author: String(comment.authorAgentId ?? comment.authorUserId ?? comment.author ?? '-'),
|
|
142
150
|
body,
|
|
143
151
|
parsed,
|
|
144
152
|
unattributed: unattributedBlockLines(body),
|
|
153
|
+
supersededCount: 0,
|
|
145
154
|
};
|
|
146
155
|
}
|
|
147
|
-
|
|
156
|
+
if (newest)
|
|
157
|
+
newest.supersededCount = blocks - 1;
|
|
158
|
+
return newest;
|
|
148
159
|
}
|
|
149
160
|
/** Last message the agent (not the user) produced in the execution conversation. */
|
|
150
161
|
function lastAssistantMessage(messages) {
|
package/dist/lib/tasks.d.ts
CHANGED
|
@@ -194,3 +194,37 @@ export declare function normalizeBulkUpdateInput(input: unknown): Array<{
|
|
|
194
194
|
id: string;
|
|
195
195
|
patch: TaskRecord;
|
|
196
196
|
}>;
|
|
197
|
+
/**
|
|
198
|
+
* Verdict strings the server recognises on a task completion.
|
|
199
|
+
*
|
|
200
|
+
* `--verdict` is free text: `skrr tasks complete <id> --verdict banana`
|
|
201
|
+
* succeeds and the value is permanently recorded, while `--status` beside it
|
|
202
|
+
* is enumerated and rejects a typo. So `--verdict aproved` closes a task with
|
|
203
|
+
* a verdict nothing matches, quietly.
|
|
204
|
+
*
|
|
205
|
+
* It cannot simply be enumerated, and that is the point worth recording:
|
|
206
|
+
*
|
|
207
|
+
* - A REVIEWER task legitimately passes a JSON object
|
|
208
|
+
* (`{"posture":"request_changes","confidence":0.82,"criteria":[…]}`) —
|
|
209
|
+
* documented in the task-execution prompt and consumed by
|
|
210
|
+
* `normalizeReviewPosture`. `options:` on the flag would refuse it.
|
|
211
|
+
* - That normaliser accepts synonyms, and when it recognises NOTHING it does
|
|
212
|
+
* not fail — it derives the posture from the criteria instead. So a typo
|
|
213
|
+
* silently becomes whatever the criteria imply.
|
|
214
|
+
*
|
|
215
|
+
* Hence a warning, not a refusal, over the set the server actually matches.
|
|
216
|
+
* Mirrored from `normalizeReviewPosture` in
|
|
217
|
+
* `api/server/services/TasksMCP/taskExecutionHelpers.js` and pinned against it
|
|
218
|
+
* by `task-verdict-parity.spec.ts`, the same way TASK_WORK_EVENT_KINDS is.
|
|
219
|
+
*/
|
|
220
|
+
export declare const RECOGNIZED_TASK_VERDICTS: readonly ["request_changes", "changes_requested", "needs_changes", "needs_revision", "approved", "approve", "pass", "passed", "blocked"];
|
|
221
|
+
/** The server's own normalisation, so the same spellings match here. */
|
|
222
|
+
export declare function normalizeVerdict(value: string): string;
|
|
223
|
+
/**
|
|
224
|
+
* Does this verdict mean anything to the server?
|
|
225
|
+
*
|
|
226
|
+
* `UNVERIFIED` is platform-stamped rather than user-supplied, and a JSON
|
|
227
|
+
* verdict is the reviewer shape — both are legitimate and neither is in the
|
|
228
|
+
* synonym list.
|
|
229
|
+
*/
|
|
230
|
+
export declare function verdictIsRecognized(value: string | undefined): boolean;
|