@skrr-ai/cli 0.1.46 → 0.1.47
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/commands/code/handover.d.ts +2 -2
- package/dist/commands/code/handover.js +12 -13
- package/dist/commands/code/install.js +31 -0
- package/dist/commands/code/update.d.ts +46 -0
- package/dist/commands/code/update.js +242 -0
- package/dist/commands/convos/handover.d.ts +1 -0
- package/dist/commands/convos/handover.js +4 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/update.d.ts +19 -0
- package/dist/commands/daemon/update.js +48 -0
- package/dist/commands/spaces/briefs/get.js +8 -0
- package/dist/commands/spaces/briefs/list.js +2 -0
- package/dist/commands/spaces/briefs/set.d.ts +3 -0
- package/dist/commands/spaces/briefs/set.js +78 -13
- package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
- package/dist/commands/spaces/expertise-suggestions.js +71 -0
- package/dist/commands/spaces/members/add.d.ts +1 -0
- package/dist/commands/spaces/members/add.js +11 -3
- package/dist/commands/spaces/members/list.js +11 -0
- package/dist/commands/tasks/assign.d.ts +9 -0
- package/dist/commands/tasks/assign.js +108 -1
- package/dist/lib/assignee-resolver.d.ts +22 -0
- package/dist/lib/assignee-resolver.js +26 -0
- package/dist/lib/code-handover.d.ts +0 -10
- package/dist/lib/code-handover.js +20 -5
- package/dist/lib/update-check.d.ts +80 -7
- package/dist/lib/update-check.js +365 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +8 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +24 -11
- package/oclif.manifest.json +10347 -10129
- package/package.json +1 -1
|
@@ -25,8 +25,8 @@ import { BaseCommand } from '../../base-command';
|
|
|
25
25
|
*
|
|
26
26
|
* The transcript CAN travel once the engine implements the session-export
|
|
27
27
|
* contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
|
|
28
|
-
* `
|
|
29
|
-
* stdout. This flag runs exactly that contract and folds the export into the
|
|
28
|
+
* `session export <id> --format json` on the engine writes a versioned envelope
|
|
29
|
+
* to stdout. This flag runs exactly that contract and folds the export into the
|
|
30
30
|
* prompt — the endpoint's only carrier. An engine that predates the contract
|
|
31
31
|
* is a clean refusal, never a silent fallback to brief-only.
|
|
32
32
|
*
|
|
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
7
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
7
8
|
const base_command_1 = require("../../base-command");
|
|
8
9
|
const code_handover_1 = require("../../lib/code-handover");
|
|
9
10
|
const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
@@ -33,8 +34,8 @@ const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
|
33
34
|
*
|
|
34
35
|
* The transcript CAN travel once the engine implements the session-export
|
|
35
36
|
* contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
|
|
36
|
-
* `
|
|
37
|
-
* stdout. This flag runs exactly that contract and folds the export into the
|
|
37
|
+
* `session export <id> --format json` on the engine writes a versioned envelope
|
|
38
|
+
* to stdout. This flag runs exactly that contract and folds the export into the
|
|
38
39
|
* prompt — the endpoint's only carrier. An engine that predates the contract
|
|
39
40
|
* is a clean refusal, never a silent fallback to brief-only.
|
|
40
41
|
*
|
|
@@ -77,15 +78,15 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
77
78
|
description: 'Give the receiving agent a browser. Refused where the deployment has no browser runtime, never run without one.',
|
|
78
79
|
}),
|
|
79
80
|
'carry-session': core_1.Flags.boolean({
|
|
80
|
-
description:
|
|
81
|
-
'handover. Opt-in because carrying a whole session is a cost the
|
|
82
|
-
'Refused outright when the engine predates the export contract —
|
|
83
|
-
'fallback to brief-only.',
|
|
81
|
+
description: `Export the engine session transcript (\`${auth_core_1.FIRST_PARTY_HARNESS.provider} session export\`) ` +
|
|
82
|
+
'and carry it into the handover. Opt-in because carrying a whole session is a cost the ' +
|
|
83
|
+
'user should choose. Refused outright when the engine predates the export contract — ' +
|
|
84
|
+
'there is no silent fallback to brief-only.',
|
|
84
85
|
default: false,
|
|
85
86
|
}),
|
|
86
87
|
session: core_1.Flags.string({
|
|
87
88
|
description: 'Engine session id to export with --carry-session (default: the session id the engine ' +
|
|
88
|
-
|
|
89
|
+
`exports to its environment — ${(0, auth_core_1.firstPartyHarnessEngineEnvNames)('sessionId').join(' or ')})`,
|
|
89
90
|
}),
|
|
90
91
|
json: core_1.Flags.boolean({ description: 'Output the created task as JSON' }),
|
|
91
92
|
};
|
|
@@ -142,12 +143,10 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
142
143
|
this.error((0, first_party_harness_1.notInstalledMessage)());
|
|
143
144
|
// `--session` wins; otherwise the variable the engine stamps into
|
|
144
145
|
// processes it spawns (current namespace, then the legacy spelling).
|
|
145
|
-
const sessionId = flags.session ||
|
|
146
|
-
process.env.SKRR_CODE_SESSION_ID?.trim() ||
|
|
147
|
-
process.env.SKY_CODE_SESSION_ID?.trim();
|
|
146
|
+
const sessionId = flags.session || (0, auth_core_1.readFirstPartyHarnessEngineEnv)(process.env, 'sessionId').value;
|
|
148
147
|
if (!sessionId) {
|
|
149
148
|
this.error('--carry-session needs the engine session to carry. Pass --session <engine-session-id>, ' +
|
|
150
|
-
|
|
149
|
+
`or run inside a session that exports ${(0, auth_core_1.firstPartyHarnessEngineEnvNames)('sessionId')[0]}.`);
|
|
151
150
|
}
|
|
152
151
|
carriedSessionId = sessionId;
|
|
153
152
|
let stdout;
|
|
@@ -165,8 +164,8 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
165
164
|
this.error(`No engine session "${sessionId}" — the engine found nothing to carry.\n` +
|
|
166
165
|
` ${failure.detail}`);
|
|
167
166
|
}
|
|
168
|
-
this.error(
|
|
169
|
-
|
|
167
|
+
this.error(`This engine does not support session export — --carry-session requires a ` +
|
|
168
|
+
`${auth_core_1.FIRST_PARTY_HARNESS_ENGINE.appName} engine implementing the session-export contract ` +
|
|
170
169
|
'(docs/architecture/sky-code-session-export-contract-2026-08-21.md).\n' +
|
|
171
170
|
` ${failure.detail}\n` +
|
|
172
171
|
' Re-run without --carry-session to hand over the situation only, or upgrade the engine.');
|
|
@@ -57,6 +57,37 @@ class CodeInstall extends base_command_1.BaseCommand {
|
|
|
57
57
|
catch (err) {
|
|
58
58
|
this.handleApiError(err);
|
|
59
59
|
}
|
|
60
|
+
// The daemon distinguishes three refusals, and only one of them attaches a
|
|
61
|
+
// job worth following:
|
|
62
|
+
//
|
|
63
|
+
// - `alreadyInstalled` carries the LAST job the provider ran — context, not
|
|
64
|
+
// this request's work. Following it replays a stale outcome as though it
|
|
65
|
+
// had just happened: `code install` on an engine one release behind
|
|
66
|
+
// reprinted the previous update's log tail verbatim and reported that job
|
|
67
|
+
// as its result (OSK-10650). The right answer is the daemon's own
|
|
68
|
+
// installDisabledReason plus where the update actually comes from.
|
|
69
|
+
// - `alreadyRunning` carries the LIVE job — following it attaches the user
|
|
70
|
+
// to work that is really in flight, which is what they asked for.
|
|
71
|
+
// - `started` carries the job this request began.
|
|
72
|
+
if (started.alreadyInstalled) {
|
|
73
|
+
if (flags.json) {
|
|
74
|
+
this.log(JSON.stringify({ daemonId, ...started }, null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const state = started.provider;
|
|
78
|
+
const reason = state?.installDisabledReason?.trim();
|
|
79
|
+
const version = state?.version?.trim();
|
|
80
|
+
this.log(`Nothing to do on ${daemonId} — ${reason || 'Already installed.'}${version ? ` (${version})` : ''}`);
|
|
81
|
+
if (state?.updatable === true) {
|
|
82
|
+
this.log(`To update it: ${this.config.bin} code update`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const why = state?.updateDisabledReason?.trim();
|
|
86
|
+
if (why)
|
|
87
|
+
this.log(`Updating: ${why}`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
60
91
|
const jobId = started.job?.id;
|
|
61
92
|
// A 202 carrying NO job is not a queued install. It is the daemon
|
|
62
93
|
// acknowledging the call and returning nothing, and the route builds its
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command';
|
|
2
|
+
/**
|
|
3
|
+
* `skrr code update` — pull the engine's signed release down through the daemon.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this command exists ────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The update verb has been API-exposed and daemon-implemented since OSK-10096 —
|
|
8
|
+
* `POST /api/daemons/:id/cli-installers/:provider/update` re-fetches the signed
|
|
9
|
+
* manifest and installs whatever the channel points at. But the only CLI that
|
|
10
|
+
* reached it was `skrr harnesses install <provider> --update`: a flag on a
|
|
11
|
+
* command a `code` user has no reason to read. On stable that left no
|
|
12
|
+
* discoverable path at all — `code install` answered "Already installed." with
|
|
13
|
+
* no next step, and the channel moving past the installed build was invisible
|
|
14
|
+
* from the surface that owns the engine (OSK-10650).
|
|
15
|
+
*
|
|
16
|
+
* So this adds a SURFACE and no mechanism. Same route `harnesses install
|
|
17
|
+
* --update` posts, same inventory pre-check, same 504 recovery — one update
|
|
18
|
+
* path, spelled where the engine's users look.
|
|
19
|
+
*/
|
|
20
|
+
export default class CodeUpdate extends BaseCommand {
|
|
21
|
+
static description: string;
|
|
22
|
+
static examples: string[];
|
|
23
|
+
static flags: {
|
|
24
|
+
daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
25
|
+
wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
26
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
27
|
+
};
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
/** Which daemon the update targets, by the rule every daemon-picking command shares. */
|
|
30
|
+
private resolveTarget;
|
|
31
|
+
/**
|
|
32
|
+
* The daemon id to update through, or the refusal — identical rules to
|
|
33
|
+
* `code install`: a Dedicated Runtime's own machine is image-managed, several
|
|
34
|
+
* connected daemons get named rather than guessed.
|
|
35
|
+
*/
|
|
36
|
+
private updateTarget;
|
|
37
|
+
/**
|
|
38
|
+
* Start the update, and survive a gateway that cut the request off — the same
|
|
39
|
+
* 504 recovery `harnesses install --update` uses: the relay can drop the
|
|
40
|
+
* answer while the daemon runs the job it started, so on a server-side
|
|
41
|
+
* failure we ask the daemon and follow the job it reports.
|
|
42
|
+
*/
|
|
43
|
+
private start;
|
|
44
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
45
|
+
private follow;
|
|
46
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
5
|
+
const base_command_1 = require("../../base-command");
|
|
6
|
+
const cli_installers_1 = require("../../lib/cli-installers");
|
|
7
|
+
const daemon_target_1 = require("../../lib/daemon-target");
|
|
8
|
+
const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
9
|
+
/**
|
|
10
|
+
* `skrr code update` — pull the engine's signed release down through the daemon.
|
|
11
|
+
*
|
|
12
|
+
* ── Why this command exists ────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* The update verb has been API-exposed and daemon-implemented since OSK-10096 —
|
|
15
|
+
* `POST /api/daemons/:id/cli-installers/:provider/update` re-fetches the signed
|
|
16
|
+
* manifest and installs whatever the channel points at. But the only CLI that
|
|
17
|
+
* reached it was `skrr harnesses install <provider> --update`: a flag on a
|
|
18
|
+
* command a `code` user has no reason to read. On stable that left no
|
|
19
|
+
* discoverable path at all — `code install` answered "Already installed." with
|
|
20
|
+
* no next step, and the channel moving past the installed build was invisible
|
|
21
|
+
* from the surface that owns the engine (OSK-10650).
|
|
22
|
+
*
|
|
23
|
+
* So this adds a SURFACE and no mechanism. Same route `harnesses install
|
|
24
|
+
* --update` posts, same inventory pre-check, same 504 recovery — one update
|
|
25
|
+
* path, spelled where the engine's users look.
|
|
26
|
+
*/
|
|
27
|
+
class CodeUpdate extends base_command_1.BaseCommand {
|
|
28
|
+
static description = `Update the ${auth_core_1.FIRST_PARTY_HARNESS.displayName} engine on this machine, through the daemon`;
|
|
29
|
+
static examples = [
|
|
30
|
+
'<%= config.bin %> code update',
|
|
31
|
+
'<%= config.bin %> code update --daemon <daemon-id>',
|
|
32
|
+
'<%= config.bin %> code update --json',
|
|
33
|
+
];
|
|
34
|
+
static flags = {
|
|
35
|
+
daemon: core_1.Flags.string({
|
|
36
|
+
description: 'Which daemon runs the update. Defaults to this machine on a Dedicated Runtime, otherwise to the only active one; required when you have several.',
|
|
37
|
+
}),
|
|
38
|
+
wait: core_1.Flags.boolean({
|
|
39
|
+
default: true,
|
|
40
|
+
allowNo: true,
|
|
41
|
+
description: 'Follow the update job until it finishes. --no-wait returns the job id.',
|
|
42
|
+
}),
|
|
43
|
+
json: core_1.Flags.boolean({ description: 'Emit the result as JSON' }),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(CodeUpdate);
|
|
47
|
+
const daemonId = this.updateTarget(await this.resolveTarget(flags.daemon));
|
|
48
|
+
// Ask the inventory before posting: a daemon older than managed updates has
|
|
49
|
+
// no handler for the request and would leave it to time out, and an engine
|
|
50
|
+
// that updates some other way has a reason the daemon already words.
|
|
51
|
+
let inventory;
|
|
52
|
+
try {
|
|
53
|
+
inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
this.handleApiError(err);
|
|
57
|
+
}
|
|
58
|
+
const row = (inventory.providers ?? []).find((p) => (0, auth_core_1.isFirstPartyHarnessProvider)(p.provider));
|
|
59
|
+
if (!row) {
|
|
60
|
+
this.failWithCliError({
|
|
61
|
+
message: `${auth_core_1.FIRST_PARTY_HARNESS.displayName} is not installed on ${daemonId}. ` +
|
|
62
|
+
`Install it with \`${this.config.bin} code install\`.`,
|
|
63
|
+
code: 'NOT_INSTALLED',
|
|
64
|
+
exit: 1,
|
|
65
|
+
retryable: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (row.updatable === undefined) {
|
|
69
|
+
this.failWithCliError({
|
|
70
|
+
message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${auth_core_1.FIRST_PARTY_HARNESS.displayName}: it predates managed updates. ` +
|
|
71
|
+
`Update the daemon, then run this again.`,
|
|
72
|
+
code: 'DAEMON_UPDATE_UNSUPPORTED',
|
|
73
|
+
exit: 1,
|
|
74
|
+
retryable: false,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!row.updatable) {
|
|
78
|
+
this.failWithCliError({
|
|
79
|
+
message: row.updateDisabledReason?.trim() ||
|
|
80
|
+
`${auth_core_1.FIRST_PARTY_HARNESS.displayName}: the daemon cannot update it here.`,
|
|
81
|
+
code: 'CLI_NOT_UPDATABLE',
|
|
82
|
+
exit: 1,
|
|
83
|
+
retryable: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const start = await this.start(daemonId, flags.json === true);
|
|
87
|
+
if (!start)
|
|
88
|
+
return;
|
|
89
|
+
const jobId = start.job?.id;
|
|
90
|
+
if (!start.started || !jobId) {
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
this.log(JSON.stringify({ daemonId, ...start }, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.log(start.alreadyRunning
|
|
96
|
+
? `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
|
|
97
|
+
: `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!flags.wait) {
|
|
101
|
+
if (flags.json) {
|
|
102
|
+
this.log(JSON.stringify({ daemonId, ...start }, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.log(`Update started on ${daemonId} — job ${jobId}.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!flags.json) {
|
|
109
|
+
this.logToStderr(`Updating ${auth_core_1.FIRST_PARTY_HARNESS.displayName} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
|
|
110
|
+
}
|
|
111
|
+
const job = await this.follow(daemonId, jobId, flags.json === true);
|
|
112
|
+
if (!job)
|
|
113
|
+
return;
|
|
114
|
+
if (flags.json) {
|
|
115
|
+
this.log(JSON.stringify({ daemonId, job }, null, 2));
|
|
116
|
+
if (job.status !== 'succeeded')
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (job.status === 'failed') {
|
|
121
|
+
this.failWithCliError({
|
|
122
|
+
message: job.error || 'The update failed and the daemon gave no reason.',
|
|
123
|
+
code: 'UPDATE_FAILED',
|
|
124
|
+
exit: 1,
|
|
125
|
+
retryable: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
|
|
129
|
+
`${auth_core_1.FIRST_PARTY_HARNESS.displayName} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
|
|
130
|
+
}
|
|
131
|
+
/** Which daemon the update targets, by the rule every daemon-picking command shares. */
|
|
132
|
+
async resolveTarget(explicit) {
|
|
133
|
+
try {
|
|
134
|
+
return await (0, daemon_target_1.resolveDaemonTarget)({
|
|
135
|
+
explicit,
|
|
136
|
+
local: (0, daemon_target_1.localMachine)({ baseURL: this.cliConfig?.baseURL }),
|
|
137
|
+
listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.handleApiError(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The daemon id to update through, or the refusal — identical rules to
|
|
146
|
+
* `code install`: a Dedicated Runtime's own machine is image-managed, several
|
|
147
|
+
* connected daemons get named rather than guessed.
|
|
148
|
+
*/
|
|
149
|
+
updateTarget(target) {
|
|
150
|
+
if (target.kind === 'daemon')
|
|
151
|
+
return target.daemonId;
|
|
152
|
+
if (target.kind === 'this-machine') {
|
|
153
|
+
this.failWithCliError({
|
|
154
|
+
message: target.explicit
|
|
155
|
+
? first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE
|
|
156
|
+
: `${first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE} This command updates the machine it runs on; ` +
|
|
157
|
+
'to update another of your machines, name it with --daemon.',
|
|
158
|
+
code: 'DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE',
|
|
159
|
+
exit: 1,
|
|
160
|
+
retryable: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (target.kind === 'none') {
|
|
164
|
+
this.failWithCliError({
|
|
165
|
+
message: 'No daemon is connected for this account, so nothing can perform the update. ' +
|
|
166
|
+
`Start the background service on this computer with \`${this.config.bin} daemon start\`, then run this again.`,
|
|
167
|
+
code: 'NO_DAEMON',
|
|
168
|
+
exit: 1,
|
|
169
|
+
retryable: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const { daemons } = target;
|
|
173
|
+
const choices = daemons
|
|
174
|
+
.slice(0, 10)
|
|
175
|
+
.map((d) => ` ${d.id} ${d.name || '—'}`)
|
|
176
|
+
.join('\n');
|
|
177
|
+
this.failWithCliError({
|
|
178
|
+
message: `${daemons.length} connected daemons — say which machine should update the engine:\n${choices}\n` +
|
|
179
|
+
`Pass one with --daemon, or see each daemon id in the DAEMON ID column of \`${this.config.bin} harnesses list\`.`,
|
|
180
|
+
code: 'AMBIGUOUS_DAEMON',
|
|
181
|
+
exit: 1,
|
|
182
|
+
retryable: false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Start the update, and survive a gateway that cut the request off — the same
|
|
187
|
+
* 504 recovery `harnesses install --update` uses: the relay can drop the
|
|
188
|
+
* answer while the daemon runs the job it started, so on a server-side
|
|
189
|
+
* failure we ask the daemon and follow the job it reports.
|
|
190
|
+
*/
|
|
191
|
+
async start(daemonId, json) {
|
|
192
|
+
try {
|
|
193
|
+
return await (0, cli_installers_1.startManagedCliUpdate)(daemonId, PROVIDER, this.resolvedCredential);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const status = err.status ?? 0;
|
|
197
|
+
if (status >= 500 || status === 408) {
|
|
198
|
+
const job = await (0, cli_installers_1.jobStartedDespiteTimeout)(daemonId, PROVIDER, this.resolvedCredential);
|
|
199
|
+
if (job?.id) {
|
|
200
|
+
if (!json) {
|
|
201
|
+
this.logToStderr(`The request to ${daemonId} did not come back (HTTP ${status}), but the daemon started job ${job.id}. Following it.`);
|
|
202
|
+
}
|
|
203
|
+
return { started: true, job };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.handleApiError(err);
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
211
|
+
async follow(daemonId, jobId, json) {
|
|
212
|
+
const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
|
|
213
|
+
daemonId,
|
|
214
|
+
jobId,
|
|
215
|
+
credential: this.resolvedCredential,
|
|
216
|
+
onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
|
|
217
|
+
});
|
|
218
|
+
if (outcome.kind === 'missing') {
|
|
219
|
+
this.failWithCliError({
|
|
220
|
+
message: `The daemon no longer has update job ${jobId}.`,
|
|
221
|
+
code: 'JOB_MISSING',
|
|
222
|
+
exit: 1,
|
|
223
|
+
retryable: false,
|
|
224
|
+
});
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
if (outcome.kind === 'timeout') {
|
|
228
|
+
this.failWithCliError({
|
|
229
|
+
message: `Still updating after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is ` +
|
|
230
|
+
`still working on job ${jobId}. Check \`${this.config.bin} code doctor\` in a while.`,
|
|
231
|
+
code: 'UPDATE_PENDING',
|
|
232
|
+
exit: 1,
|
|
233
|
+
retryable: true,
|
|
234
|
+
});
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return outcome.job;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.default = CodeUpdate;
|
|
241
|
+
/** Same canonical provider id `code install` posts — read from the identity module. */
|
|
242
|
+
const PROVIDER = auth_core_1.FIRST_PARTY_HARNESS.provider;
|
|
@@ -24,6 +24,7 @@ export default class ConvosHandover extends BaseCommand {
|
|
|
24
24
|
to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
25
25
|
'up-to': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
26
26
|
reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
27
|
+
'allow-recall': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
27
28
|
};
|
|
28
29
|
run(): Promise<void>;
|
|
29
30
|
}
|
|
@@ -44,6 +44,9 @@ class ConvosHandover extends base_command_1.BaseCommand {
|
|
|
44
44
|
reason: core_1.Flags.string({
|
|
45
45
|
description: 'Why the work is moving — recorded in provenance and the forward link',
|
|
46
46
|
}),
|
|
47
|
+
'allow-recall': core_1.Flags.boolean({
|
|
48
|
+
description: 'Grant the receiving agent a time-boxed (7d) re-read of this thread after handover',
|
|
49
|
+
}),
|
|
47
50
|
};
|
|
48
51
|
async run() {
|
|
49
52
|
this.requireAuth();
|
|
@@ -74,6 +77,7 @@ class ConvosHandover extends base_command_1.BaseCommand {
|
|
|
74
77
|
targetAgentId: flags.to,
|
|
75
78
|
upTo: flags['up-to'],
|
|
76
79
|
reason: flags.reason,
|
|
80
|
+
allowRecall: flags['allow-recall'] === true ? true : undefined,
|
|
77
81
|
});
|
|
78
82
|
}
|
|
79
83
|
catch (err) {
|
|
@@ -26,6 +26,7 @@ class Daemon extends core_1.Command {
|
|
|
26
26
|
'<%= config.bin %> daemon status',
|
|
27
27
|
'<%= config.bin %> daemon start',
|
|
28
28
|
'<%= config.bin %> daemon stop',
|
|
29
|
+
'<%= config.bin %> daemon update',
|
|
29
30
|
'<%= config.bin %> daemon logs',
|
|
30
31
|
'<%= config.bin %> daemon recover-offline-queue --apply',
|
|
31
32
|
'<%= config.bin %> daemon usage --refresh',
|
|
@@ -44,6 +45,7 @@ class Daemon extends core_1.Command {
|
|
|
44
45
|
this.log(' skrr daemon uninstall — remove that service (local state is left alone)');
|
|
45
46
|
this.log(' skrr daemon start — start the daemon');
|
|
46
47
|
this.log(' skrr daemon stop — stop the daemon');
|
|
48
|
+
this.log(' skrr daemon update — update the daemon binary to the latest signed release');
|
|
47
49
|
this.log(' skrr daemon status — show running state and config');
|
|
48
50
|
this.log(' skrr daemon logs — tail recent daemon logs');
|
|
49
51
|
this.log(' skrr daemon recover-offline-queue — restore recoverable offline WALs');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* `skrr daemon update` — update the runtime binary on this computer.
|
|
4
|
+
*
|
|
5
|
+
* Proxied like every other lifecycle verb. The earlier exclusion reason — that
|
|
6
|
+
* a self-update "must not run from inside a wrapper it may replace" — did not
|
|
7
|
+
* hold: `skrr` is a separate Node process that spawns `skrrd update`; the
|
|
8
|
+
* verified swap targets the daemon BINARY on disk, never the running CLI. The
|
|
9
|
+
* running daemon process is likewise untouched — the self-upgrade monitor
|
|
10
|
+
* restarts it afterwards, exactly as it does for the background poller.
|
|
11
|
+
*/
|
|
12
|
+
export default class DaemonUpdate extends Command {
|
|
13
|
+
static aliases: string[];
|
|
14
|
+
static summary: string;
|
|
15
|
+
static description: string;
|
|
16
|
+
static strict: boolean;
|
|
17
|
+
static examples: string[];
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
|
|
5
|
+
/**
|
|
6
|
+
* `skrr daemon update` — update the runtime binary on this computer.
|
|
7
|
+
*
|
|
8
|
+
* Proxied like every other lifecycle verb. The earlier exclusion reason — that
|
|
9
|
+
* a self-update "must not run from inside a wrapper it may replace" — did not
|
|
10
|
+
* hold: `skrr` is a separate Node process that spawns `skrrd update`; the
|
|
11
|
+
* verified swap targets the daemon BINARY on disk, never the running CLI. The
|
|
12
|
+
* running daemon process is likewise untouched — the self-upgrade monitor
|
|
13
|
+
* restarts it afterwards, exactly as it does for the background poller.
|
|
14
|
+
*/
|
|
15
|
+
class DaemonUpdate extends core_1.Command {
|
|
16
|
+
// `skrr daemons <sub>` — the plural is advertised as an alias for the
|
|
17
|
+
// whole topic, and without a per-command alias only the bare topic
|
|
18
|
+
// resolved: every subcommand under it failed.
|
|
19
|
+
static aliases = ['daemons:update'];
|
|
20
|
+
static summary = 'Update the skrr daemon binary on this computer to the latest signed release';
|
|
21
|
+
static description = (0, exec_runtime_binary_1.forwardedFlagsNote)('update');
|
|
22
|
+
static strict = false; // forward all args verbatim — see run()
|
|
23
|
+
static examples = [
|
|
24
|
+
'<%= config.bin %> daemon update',
|
|
25
|
+
'<%= config.bin %> daemon update --check',
|
|
26
|
+
'<%= config.bin %> daemon update --target-version 0.8.54',
|
|
27
|
+
'<%= config.bin %> daemon update --restart',
|
|
28
|
+
];
|
|
29
|
+
async run() {
|
|
30
|
+
// `this.argv`, NOT `this.parse()`. `strict = false` relaxes POSITIONAL
|
|
31
|
+
// arity and oclif still rejects any flag this command has not declared —
|
|
32
|
+
// so every `skrrd` flag would be unreachable through the wrapper that
|
|
33
|
+
// exists to reach it (OSK-296).
|
|
34
|
+
const argv = this.argv;
|
|
35
|
+
try {
|
|
36
|
+
const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['update', ...argv]);
|
|
37
|
+
this.exit(code);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err.message === 'runtime-binary-not-installed') {
|
|
41
|
+
this.log((0, exec_runtime_binary_1.notInstalledMessage)());
|
|
42
|
+
this.exit(127);
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.default = DaemonUpdate;
|
|
@@ -38,9 +38,17 @@ class SpacesBriefsGet extends base_command_1.BaseCommand {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
const b = result;
|
|
41
|
+
const expertise = Array.isArray(b.expertise) ? b.expertise : [];
|
|
42
|
+
const dismissed = Array.isArray(b.dismissedExpertiseSuggestions)
|
|
43
|
+
? b.dismissedExpertiseSuggestions
|
|
44
|
+
: [];
|
|
41
45
|
this.log(`Principal Type: ${b.principalType ?? flags.principal}`);
|
|
42
46
|
this.log(`Principal ID: ${b.principalId ?? flags.id}`);
|
|
43
47
|
this.log(`Updated: ${b.updatedAt ? (0, format_1.localTime)(b.updatedAt) : '-'}`);
|
|
48
|
+
this.log(`Expertise: ${expertise.length > 0 ? expertise.join(', ') : '-'}`);
|
|
49
|
+
if (dismissed.length > 0) {
|
|
50
|
+
this.log(`Dismissed: ${dismissed.join(', ')}`);
|
|
51
|
+
}
|
|
44
52
|
this.log('');
|
|
45
53
|
this.log('Content:');
|
|
46
54
|
const content = typeof b.content === 'string' ? b.content : '';
|
|
@@ -39,12 +39,14 @@ class SpacesBriefsList extends base_command_1.BaseCommand {
|
|
|
39
39
|
const rows = items.map((b) => ({
|
|
40
40
|
principalType: String(b.principalType ?? '-'),
|
|
41
41
|
principalId: String(b.principalId ?? '-'),
|
|
42
|
+
expertise: Array.isArray(b.expertise) ? b.expertise.join(', ') : '-',
|
|
42
43
|
length: String(typeof b.content === 'string' ? b.content.length : 0),
|
|
43
44
|
updated: b.updatedAt ? (0, format_1.localTime)(b.updatedAt) : '-',
|
|
44
45
|
}));
|
|
45
46
|
(0, format_1.renderTable)(rows, [
|
|
46
47
|
{ key: 'principalType', header: 'PRINCIPAL TYPE' },
|
|
47
48
|
{ key: 'principalId', header: 'PRINCIPAL ID' },
|
|
49
|
+
{ key: 'expertise', header: 'EXPERTISE', maxWidth: 40 },
|
|
48
50
|
{ key: 'length', header: 'LENGTH' },
|
|
49
51
|
{ key: 'updated', header: 'UPDATED' },
|
|
50
52
|
], (line) => this.log(line));
|
|
@@ -11,6 +11,9 @@ export default class SpacesBriefsSet extends BaseCommand {
|
|
|
11
11
|
id: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
12
|
content: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
13
|
'content-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
|
+
expertise: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
|
+
'add-expertise': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
16
|
+
'dismiss-expertise': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
14
17
|
'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
15
18
|
};
|
|
16
19
|
run(): Promise<void>;
|