@skrr-ai/cli 0.1.41 → 0.1.42
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/install.js +3 -4
- package/dist/commands/followups/remind.js +4 -0
- package/dist/commands/harnesses/install.d.ts +18 -0
- package/dist/commands/harnesses/install.js +128 -19
- package/dist/commands/skills/import-as-actions.js +6 -15
- package/dist/commands/tasks/complete.js +13 -0
- package/dist/commands/tasks/result/submit.js +10 -5
- package/dist/lib/cli-installers.d.ts +40 -0
- package/dist/lib/cli-installers.js +51 -1
- package/dist/lib/followups.d.ts +2 -0
- package/dist/lib/local-skills.d.ts +27 -0
- package/dist/lib/local-skills.js +38 -0
- package/dist/lib/task-closure.d.ts +13 -0
- package/dist/lib/task-closure.js +17 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1780 -1769
- package/oclif.manifest.json +32199 -32192
- package/package.json +1 -1
|
@@ -4,7 +4,6 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
5
5
|
const base_command_1 = require("../../base-command");
|
|
6
6
|
const api_fetch_1 = require("../../lib/api-fetch");
|
|
7
|
-
const daemon_ref_1 = require("../../lib/daemon-ref");
|
|
8
7
|
const daemon_target_1 = require("../../lib/daemon-target");
|
|
9
8
|
const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
10
9
|
/**
|
|
@@ -84,11 +83,11 @@ class CodeInstall extends base_command_1.BaseCommand {
|
|
|
84
83
|
// survive in mine.
|
|
85
84
|
const reason = await this.installerNote(daemonId);
|
|
86
85
|
if (reason) {
|
|
87
|
-
this.log(`Nothing to do on ${
|
|
86
|
+
this.log(`Nothing to do on ${daemonId} — ${reason}`);
|
|
88
87
|
return;
|
|
89
88
|
}
|
|
90
89
|
this.failWithCliError({
|
|
91
|
-
message: `The daemon on ${
|
|
90
|
+
message: `The daemon on ${daemonId} accepted the request and returned no ` +
|
|
92
91
|
'install job, and does not say why. Nothing has been shown to start. ' +
|
|
93
92
|
`Check what it offers with \`${this.config.bin} harnesses installers --daemon ${daemonId}\`, and ` +
|
|
94
93
|
`\`${this.config.bin} code doctor\` here once you expect it to have landed.`,
|
|
@@ -104,7 +103,7 @@ class CodeInstall extends base_command_1.BaseCommand {
|
|
|
104
103
|
this.log(JSON.stringify({ daemonId, ...started }, null, 2));
|
|
105
104
|
return;
|
|
106
105
|
}
|
|
107
|
-
this.log(`Install started on ${
|
|
106
|
+
this.log(`Install started on ${daemonId} — job ${jobId}.`);
|
|
108
107
|
return;
|
|
109
108
|
}
|
|
110
109
|
const job = await this.followJob(daemonId, jobId, flags.json === true);
|
|
@@ -95,6 +95,10 @@ class FollowupsRemind extends base_command_1.BaseCommand {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const row = response.followup;
|
|
98
|
+
if (response.reused && response.rescheduled) {
|
|
99
|
+
this.log(`Moved: ${row?.id} now fires at ${row?.nextRunAt ?? at} — the same reminder already existed, so it was rescheduled rather than duplicated.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
98
102
|
if (response.reused) {
|
|
99
103
|
this.log(`Already armed: ${row?.id} — the same reminder exists and was not duplicated.`);
|
|
100
104
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseCommand } from '../../base-command';
|
|
2
|
+
import { type ManagedCliProviderState } from '../../lib/cli-installers';
|
|
2
3
|
/**
|
|
3
4
|
* Install one managed coding-agent CLI on a machine.
|
|
4
5
|
*
|
|
@@ -33,9 +34,26 @@ export default class HarnessesInstall extends BaseCommand {
|
|
|
33
34
|
static flags: {
|
|
34
35
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
35
36
|
daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
37
|
+
update: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
36
38
|
};
|
|
37
39
|
run(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Update an installed CLI in place (OSK-10096).
|
|
42
|
+
*
|
|
43
|
+
* Asks the inventory first rather than posting blind: a daemon older than
|
|
44
|
+
* managed updates has no handler for the request and would leave it to time
|
|
45
|
+
* out, and a CLI that updates some other way (the copy inside Devin.app) has a
|
|
46
|
+
* reason the daemon already words. Both are answered before anything runs.
|
|
47
|
+
*/
|
|
48
|
+
private update;
|
|
49
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
50
|
+
private follow;
|
|
38
51
|
/** What the daemon says about this provider right now, best effort. */
|
|
39
52
|
private installerNote;
|
|
40
53
|
private daemonIdFor;
|
|
41
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The next step after "Already installed.": how to update it, or where its
|
|
57
|
+
* update comes from — or nothing from a daemon that cannot say (OSK-10096).
|
|
58
|
+
*/
|
|
59
|
+
export declare function updateHint(provider: string, state: ManagedCliProviderState | undefined, bin: string): string | null;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateHint = updateHint;
|
|
3
4
|
const core_1 = require("@oclif/core");
|
|
4
5
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
6
|
const base_command_1 = require("../../base-command");
|
|
6
7
|
const cli_installers_1 = require("../../lib/cli-installers");
|
|
7
|
-
const daemon_ref_1 = require("../../lib/daemon-ref");
|
|
8
8
|
const daemon_target_1 = require("../../lib/daemon-target");
|
|
9
9
|
/**
|
|
10
10
|
* Install one managed coding-agent CLI on a machine.
|
|
@@ -32,10 +32,11 @@ const daemon_target_1 = require("../../lib/daemon-target");
|
|
|
32
32
|
* come back as an opaque refusal.
|
|
33
33
|
*/
|
|
34
34
|
class HarnessesInstall extends base_command_1.BaseCommand {
|
|
35
|
-
static description = 'Install a managed coding-agent CLI (claude, codex, devin, …) on one of your machines';
|
|
35
|
+
static description = 'Install a managed coding-agent CLI (claude, codex, devin, …) on one of your machines, or update one already installed (--update)';
|
|
36
36
|
static examples = [
|
|
37
37
|
'<%= config.bin %> harnesses install devin',
|
|
38
38
|
'<%= config.bin %> harnesses install codex --daemon daemon_07c086e3c249_cli_default',
|
|
39
|
+
'<%= config.bin %> harnesses install claude --update',
|
|
39
40
|
];
|
|
40
41
|
static args = {
|
|
41
42
|
provider: core_1.Args.string({
|
|
@@ -49,6 +50,9 @@ class HarnessesInstall extends base_command_1.BaseCommand {
|
|
|
49
50
|
daemon: core_1.Flags.string({
|
|
50
51
|
description: 'Which machine to install on. Defaults to your only connected daemon; required when several are connected.',
|
|
51
52
|
}),
|
|
53
|
+
update: core_1.Flags.boolean({
|
|
54
|
+
description: "Update the CLI already installed, with that CLI's own updater. When a CLI updates some other way (e.g. inside its desktop app), the daemon says so instead.",
|
|
55
|
+
}),
|
|
52
56
|
};
|
|
53
57
|
async run() {
|
|
54
58
|
this.requireAuth();
|
|
@@ -66,6 +70,10 @@ class HarnessesInstall extends base_command_1.BaseCommand {
|
|
|
66
70
|
listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
|
|
67
71
|
});
|
|
68
72
|
const daemonId = this.daemonIdFor(target);
|
|
73
|
+
if (flags.update) {
|
|
74
|
+
await this.update(daemonId, provider, Boolean(flags.json));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
69
77
|
let start;
|
|
70
78
|
try {
|
|
71
79
|
start = await (0, cli_installers_1.startManagedCliInstall)(daemonId, provider, this.resolvedCredential);
|
|
@@ -83,27 +91,22 @@ class HarnessesInstall extends base_command_1.BaseCommand {
|
|
|
83
91
|
const reason = start.provider?.installDisabledReason?.trim() ||
|
|
84
92
|
(await this.installerNote(daemonId, provider));
|
|
85
93
|
this.log(reason ? `${provider}: ${reason}` : `${provider}: the daemon started no install job.`);
|
|
94
|
+
// "Already installed." used to be the end of the road: the product could
|
|
95
|
+
// install a CLI and then had no word about updating it (OSK-10096).
|
|
96
|
+
if (start.alreadyInstalled) {
|
|
97
|
+
const next = updateHint(provider, start.provider, this.config.bin);
|
|
98
|
+
if (next)
|
|
99
|
+
this.log(next);
|
|
100
|
+
}
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
103
|
+
// The raw id: it is what `--daemon` takes, and a formatDaemonRef prefix of a
|
|
104
|
+
// `daemon_<hash>_…` id reads `d_daemon_4...` for every local daemon (OSK-10182).
|
|
88
105
|
if (!flags.json)
|
|
89
|
-
this.logToStderr(`Installing ${provider} on ${
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
jobId,
|
|
93
|
-
credential: this.resolvedCredential,
|
|
94
|
-
onLogLine: flags.json ? undefined : (line) => this.logToStderr(` ${line}`),
|
|
95
|
-
});
|
|
96
|
-
if (outcome.kind === 'missing') {
|
|
97
|
-
this.error(`The daemon no longer has install job ${jobId}.`, { exit: 1 });
|
|
106
|
+
this.logToStderr(`Installing ${provider} on ${daemonId}…`);
|
|
107
|
+
const job = await this.follow(daemonId, jobId, 'install', Boolean(flags.json));
|
|
108
|
+
if (!job)
|
|
98
109
|
return;
|
|
99
|
-
}
|
|
100
|
-
if (outcome.kind === 'timeout') {
|
|
101
|
-
// Not a failure: the daemon is still working, and calling this failed
|
|
102
|
-
// would send someone to debug a download that is going fine.
|
|
103
|
-
this.error(`Still installing after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is still working on job ${jobId}. Check \`${this.config.bin} harnesses installers\` in a while.`, { exit: 1 });
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const job = outcome.job;
|
|
107
110
|
if (flags.json) {
|
|
108
111
|
this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
|
|
109
112
|
if (job.status !== 'succeeded')
|
|
@@ -123,6 +126,99 @@ class HarnessesInstall extends base_command_1.BaseCommand {
|
|
|
123
126
|
const state = await this.installerNote(daemonId, provider, true);
|
|
124
127
|
this.log(`${provider} installed.${state ? ` ${state}` : ''}`);
|
|
125
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Update an installed CLI in place (OSK-10096).
|
|
131
|
+
*
|
|
132
|
+
* Asks the inventory first rather than posting blind: a daemon older than
|
|
133
|
+
* managed updates has no handler for the request and would leave it to time
|
|
134
|
+
* out, and a CLI that updates some other way (the copy inside Devin.app) has a
|
|
135
|
+
* reason the daemon already words. Both are answered before anything runs.
|
|
136
|
+
*/
|
|
137
|
+
async update(daemonId, provider, json) {
|
|
138
|
+
let inventory;
|
|
139
|
+
try {
|
|
140
|
+
inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
this.handleApiError(err);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const row = (inventory.providers ?? []).find((p) => (0, data_provider_1.normalizeManagedCliProvider)(p.provider) === provider);
|
|
147
|
+
if (!row || row.updatable === undefined) {
|
|
148
|
+
this.failWithCliError({
|
|
149
|
+
message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${provider}: it predates managed updates. ` +
|
|
150
|
+
`Update the daemon, then run this again — \`${this.config.bin} daemon status\` says how.`,
|
|
151
|
+
code: 'DAEMON_UPDATE_UNSUPPORTED',
|
|
152
|
+
exit: 1,
|
|
153
|
+
retryable: false,
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!row.updatable) {
|
|
158
|
+
this.failWithCliError({
|
|
159
|
+
message: `${provider}: ${row.updateDisabledReason?.trim() || 'the daemon cannot update it here.'}`,
|
|
160
|
+
code: 'CLI_NOT_UPDATABLE',
|
|
161
|
+
exit: 1,
|
|
162
|
+
retryable: false,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let start;
|
|
167
|
+
try {
|
|
168
|
+
start = await (0, cli_installers_1.startManagedCliUpdate)(daemonId, provider, this.resolvedCredential);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
this.handleApiError(err);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const jobId = start.job?.id;
|
|
175
|
+
if (!start.started || !jobId) {
|
|
176
|
+
this.log(start.alreadyRunning
|
|
177
|
+
? `${provider}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
|
|
178
|
+
: `${provider}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!json) {
|
|
182
|
+
this.logToStderr(`Updating ${provider} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
|
|
183
|
+
}
|
|
184
|
+
const job = await this.follow(daemonId, jobId, 'update', json);
|
|
185
|
+
if (!job)
|
|
186
|
+
return;
|
|
187
|
+
if (json) {
|
|
188
|
+
this.log(JSON.stringify({ daemonId, provider, job }, null, 2));
|
|
189
|
+
if (job.status !== 'succeeded')
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (job.status === 'failed') {
|
|
194
|
+
this.error(`Updating ${provider} failed: ${job.error || 'the daemon gave no reason.'}`, {
|
|
195
|
+
exit: 1,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
|
|
200
|
+
`${provider} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
|
|
201
|
+
}
|
|
202
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
203
|
+
async follow(daemonId, jobId, verb, json) {
|
|
204
|
+
const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
|
|
205
|
+
daemonId,
|
|
206
|
+
jobId,
|
|
207
|
+
credential: this.resolvedCredential,
|
|
208
|
+
onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
|
|
209
|
+
});
|
|
210
|
+
if (outcome.kind === 'missing') {
|
|
211
|
+
this.error(`The daemon no longer has ${verb} job ${jobId}.`, { exit: 1 });
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
if (outcome.kind === 'timeout') {
|
|
215
|
+
// Not a failure: the daemon is still working, and calling this failed
|
|
216
|
+
// would send someone to debug a download that is going fine.
|
|
217
|
+
this.error(`Still ${verb === 'install' ? 'installing' : 'updating'} after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is still working on job ${jobId}. Check \`${this.config.bin} harnesses installers\` in a while.`, { exit: 1 });
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return outcome.job;
|
|
221
|
+
}
|
|
126
222
|
/** What the daemon says about this provider right now, best effort. */
|
|
127
223
|
async installerNote(daemonId, provider, readiness = false) {
|
|
128
224
|
try {
|
|
@@ -175,3 +271,16 @@ class HarnessesInstall extends base_command_1.BaseCommand {
|
|
|
175
271
|
}
|
|
176
272
|
}
|
|
177
273
|
exports.default = HarnessesInstall;
|
|
274
|
+
/**
|
|
275
|
+
* The next step after "Already installed.": how to update it, or where its
|
|
276
|
+
* update comes from — or nothing from a daemon that cannot say (OSK-10096).
|
|
277
|
+
*/
|
|
278
|
+
function updateHint(provider, state, bin) {
|
|
279
|
+
if (!state || state.updatable === undefined)
|
|
280
|
+
return null;
|
|
281
|
+
if (state.updatable) {
|
|
282
|
+
return `To update it: ${bin} harnesses install ${provider} --update`;
|
|
283
|
+
}
|
|
284
|
+
const reason = state.updateDisabledReason?.trim();
|
|
285
|
+
return reason ? `Updating: ${reason}` : null;
|
|
286
|
+
}
|
|
@@ -140,35 +140,26 @@ class SkillsImportAsActions extends base_command_1.BaseCommand {
|
|
|
140
140
|
this.handleApiError(err);
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
-
const failed =
|
|
143
|
+
const { rows, summary, failed } = (0, local_skills_1.summarizeSkillActionImport)(result);
|
|
144
144
|
if (flags.json) {
|
|
145
145
|
this.log(JSON.stringify(result, null, 2));
|
|
146
146
|
}
|
|
147
147
|
else {
|
|
148
|
-
(0, format_1.renderTable)(
|
|
149
|
-
skill: r.name,
|
|
150
|
-
import: r.status,
|
|
151
|
-
action: r.actionCreated
|
|
152
|
-
? 'created'
|
|
153
|
-
: (r.actionSkippedReason ??
|
|
154
|
-
r.actionError ??
|
|
155
|
-
(r.status === 'failed' ? '-' : 'not added')),
|
|
156
|
-
detail: r.error ?? '',
|
|
157
|
-
})), [
|
|
148
|
+
(0, format_1.renderTable)(rows, [
|
|
158
149
|
{ key: 'skill', header: 'SKILL' },
|
|
159
150
|
{ key: 'import', header: 'IMPORT' },
|
|
160
151
|
{ key: 'action', header: 'ACTION' },
|
|
161
152
|
{ key: 'detail', header: 'DETAIL' },
|
|
162
153
|
], (line) => this.log(line));
|
|
163
|
-
this.log(
|
|
164
|
-
`${failed.length} skill(s) failed.`);
|
|
154
|
+
this.log(summary);
|
|
165
155
|
if (result.actionsError) {
|
|
166
156
|
this.log(`The skills imported, but their actions could not be added: ${result.actionsError.message}`);
|
|
167
157
|
}
|
|
168
158
|
}
|
|
169
159
|
// Partial success is normal and reported above; exit non-zero only when
|
|
170
|
-
// something the caller asked for did not happen
|
|
171
|
-
|
|
160
|
+
// something the caller asked for did not happen — including an action the
|
|
161
|
+
// full library could not take.
|
|
162
|
+
if (failed)
|
|
172
163
|
this.exit(1);
|
|
173
164
|
}
|
|
174
165
|
}
|
|
@@ -255,6 +255,19 @@ class TasksComplete extends base_command_1.BaseCommand {
|
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
catch (error) {
|
|
258
|
+
// Completing records a Result, so a closed Task refuses it the same way
|
|
259
|
+
// `tasks result submit` is refused, with the same way forward.
|
|
260
|
+
const refusal = (0, base_command_1.apiRefusal)(error);
|
|
261
|
+
if (refusal.code === 'TASK_REOPEN_REQUIRED') {
|
|
262
|
+
this.error([
|
|
263
|
+
refusal.message ?? 'This Task is closed.',
|
|
264
|
+
...(0, task_closure_1.reopenRequiredLines)({
|
|
265
|
+
bin: this.config.bin,
|
|
266
|
+
task: identity.id,
|
|
267
|
+
command: 'complete',
|
|
268
|
+
}),
|
|
269
|
+
].join('\n'), { exit: 1, code: 'TASK_REOPEN_REQUIRED' });
|
|
270
|
+
}
|
|
258
271
|
this.handleApiError(error);
|
|
259
272
|
}
|
|
260
273
|
const url = (0, web_url_1.taskUrlFrom)(this.cliConfig.baseURL, { id: taskId });
|
|
@@ -122,14 +122,19 @@ class TasksResultSubmit extends base_command_1.BaseCommand {
|
|
|
122
122
|
: await data_provider_1.dataService.submitTaskResult({ taskId: task.id, data });
|
|
123
123
|
}
|
|
124
124
|
catch (error) {
|
|
125
|
-
// A closed Task refuses a Result. Name the
|
|
126
|
-
//
|
|
125
|
+
// A closed Task refuses a Result. Name the way forward here, where the
|
|
126
|
+
// commands are known: the API's own message stays surface-neutral
|
|
127
127
|
// (OSK-10031).
|
|
128
128
|
const refusal = (0, base_command_1.apiRefusal)(error);
|
|
129
129
|
if (refusal.code === 'TASK_REOPEN_REQUIRED') {
|
|
130
|
-
this.error(
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
this.error([
|
|
131
|
+
refusal.message ?? 'This Task is closed.',
|
|
132
|
+
...(0, task_closure_1.reopenRequiredLines)({
|
|
133
|
+
bin: this.config.bin,
|
|
134
|
+
task: task.id,
|
|
135
|
+
command: 'result submit',
|
|
136
|
+
}),
|
|
137
|
+
].join('\n'), { exit: 1, code: 'TASK_REOPEN_REQUIRED' });
|
|
133
138
|
}
|
|
134
139
|
this.handleApiError(error);
|
|
135
140
|
}
|
|
@@ -25,6 +25,13 @@ export interface ManagedCliProviderState {
|
|
|
25
25
|
source?: string;
|
|
26
26
|
installable?: boolean;
|
|
27
27
|
installDisabledReason?: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Whether the daemon can update this installed CLI in place, and why not.
|
|
30
|
+
* ABSENT from a daemon older than managed updates (OSK-10096) — read that as
|
|
31
|
+
* "this daemon cannot say", never as `false`.
|
|
32
|
+
*/
|
|
33
|
+
updatable?: boolean;
|
|
34
|
+
updateDisabledReason?: string | null;
|
|
28
35
|
platformSupported?: boolean;
|
|
29
36
|
authentication?: ManagedCliAuthentication | null;
|
|
30
37
|
job?: {
|
|
@@ -143,18 +150,33 @@ export declare function readinessForRows(rows: readonly ReadinessRow[], credenti
|
|
|
143
150
|
}>;
|
|
144
151
|
export interface ManagedCliInstallJob {
|
|
145
152
|
id: string;
|
|
153
|
+
provider?: string;
|
|
154
|
+
displayName?: string;
|
|
146
155
|
status: 'queued' | 'running' | 'succeeded' | 'failed';
|
|
156
|
+
/** `update` for an in-place update; absent from an older daemon, meaning install. */
|
|
157
|
+
kind?: 'install' | 'update';
|
|
158
|
+
previousVersion?: string | null;
|
|
159
|
+
installedVersion?: string | null;
|
|
147
160
|
error?: string;
|
|
148
161
|
/**
|
|
149
162
|
* The daemon's job log: the LAST lines only (a sliding window, 200 lines in
|
|
150
163
|
* `daemon/src/cli-installers.ts`). This is the field the daemon sends.
|
|
151
164
|
*/
|
|
152
165
|
logTail?: string[];
|
|
166
|
+
/**
|
|
167
|
+
* Lines the job has EVER logged (daemons with OSK-10179). With it, the lines
|
|
168
|
+
* new since the last poll are exactly the last `count − printed` of the tail,
|
|
169
|
+
* even when they repeat.
|
|
170
|
+
*/
|
|
171
|
+
logLineCount?: number;
|
|
153
172
|
/** Never sent by any daemon; kept so an older caller's shape still types. */
|
|
154
173
|
log?: string[];
|
|
155
174
|
}
|
|
156
175
|
export interface StartInstallResponse {
|
|
157
176
|
started?: boolean;
|
|
177
|
+
/** The daemon declined because the CLI is already there. */
|
|
178
|
+
alreadyInstalled?: boolean;
|
|
179
|
+
alreadyRunning?: boolean;
|
|
158
180
|
provider?: ManagedCliProviderState;
|
|
159
181
|
job?: {
|
|
160
182
|
id?: string;
|
|
@@ -162,6 +184,14 @@ export interface StartInstallResponse {
|
|
|
162
184
|
};
|
|
163
185
|
}
|
|
164
186
|
export declare function startManagedCliInstall(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
|
|
187
|
+
/** Ask the daemon to update an installed CLI in place (OSK-10096). */
|
|
188
|
+
export declare function startManagedCliUpdate(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
|
|
189
|
+
/**
|
|
190
|
+
* How an update job ended, from the versions the daemon recorded on it:
|
|
191
|
+
* "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
|
|
192
|
+
* its updater found nothing newer." Null when the job does not carry both.
|
|
193
|
+
*/
|
|
194
|
+
export declare function updateOutcomeLine(job: ManagedCliInstallJob): string | null;
|
|
165
195
|
export declare function readManagedCliInstallJob(daemonId: string, jobId: string, credential?: unknown): Promise<{
|
|
166
196
|
job?: ManagedCliInstallJob;
|
|
167
197
|
}>;
|
|
@@ -174,6 +204,16 @@ export declare function readManagedCliInstallJob(daemonId: string, jobId: string
|
|
|
174
204
|
* the end of what was already printed, and print only what follows it.
|
|
175
205
|
*/
|
|
176
206
|
export declare function unseenLogLines(printed: readonly string[], tail: readonly string[]): string[];
|
|
207
|
+
/**
|
|
208
|
+
* The new lines of a job's tail when the daemon reports its running line count:
|
|
209
|
+
* exactly the last `total − printedCount`, and how many of those already fell
|
|
210
|
+
* out of the window (the daemon keeps the last 200). Unlike overlap matching,
|
|
211
|
+
* this cannot mistake a repeated line for one already printed.
|
|
212
|
+
*/
|
|
213
|
+
export declare function newLogLinesByCount(printedCount: number, tail: readonly string[], total: number): {
|
|
214
|
+
lines: string[];
|
|
215
|
+
skipped: number;
|
|
216
|
+
};
|
|
177
217
|
/** A 100 MB download over a hotel connection is the case this has to survive. */
|
|
178
218
|
export declare const INSTALL_MAX_WAIT_MS: number;
|
|
179
219
|
export declare const INSTALL_POLL_INTERVAL_MS = 2000;
|
|
@@ -9,8 +9,11 @@ exports.readinessByProvider = readinessByProvider;
|
|
|
9
9
|
exports.readinessKey = readinessKey;
|
|
10
10
|
exports.readinessForRows = readinessForRows;
|
|
11
11
|
exports.startManagedCliInstall = startManagedCliInstall;
|
|
12
|
+
exports.startManagedCliUpdate = startManagedCliUpdate;
|
|
13
|
+
exports.updateOutcomeLine = updateOutcomeLine;
|
|
12
14
|
exports.readManagedCliInstallJob = readManagedCliInstallJob;
|
|
13
15
|
exports.unseenLogLines = unseenLogLines;
|
|
16
|
+
exports.newLogLinesByCount = newLogLinesByCount;
|
|
14
17
|
exports.followManagedCliInstallJob = followManagedCliInstallJob;
|
|
15
18
|
/**
|
|
16
19
|
* The daemon's managed-CLI installer inventory, as the CLI reads it.
|
|
@@ -211,6 +214,25 @@ async function readinessForRows(rows, credential, opts = {}) {
|
|
|
211
214
|
async function startManagedCliInstall(daemonId, provider, credential) {
|
|
212
215
|
return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/install`, { method: 'POST', body: {}, credential: credential });
|
|
213
216
|
}
|
|
217
|
+
/** Ask the daemon to update an installed CLI in place (OSK-10096). */
|
|
218
|
+
async function startManagedCliUpdate(daemonId, provider, credential) {
|
|
219
|
+
return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/update`, { method: 'POST', body: {}, credential: credential });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* How an update job ended, from the versions the daemon recorded on it:
|
|
223
|
+
* "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
|
|
224
|
+
* its updater found nothing newer." Null when the job does not carry both.
|
|
225
|
+
*/
|
|
226
|
+
function updateOutcomeLine(job) {
|
|
227
|
+
const before = job.previousVersion;
|
|
228
|
+
const after = job.installedVersion;
|
|
229
|
+
if (!before || !after)
|
|
230
|
+
return null;
|
|
231
|
+
const name = job.displayName || job.provider || 'The CLI';
|
|
232
|
+
return before === after
|
|
233
|
+
? `${name} is still ${after}: its updater found nothing newer.`
|
|
234
|
+
: `Updated ${name} ${before} → ${after}.`;
|
|
235
|
+
}
|
|
214
236
|
async function readManagedCliInstallJob(daemonId, jobId, credential) {
|
|
215
237
|
return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/jobs/${encodeURIComponent(jobId)}`, { credential: credential });
|
|
216
238
|
}
|
|
@@ -237,6 +259,20 @@ function unseenLogLines(printed, tail) {
|
|
|
237
259
|
}
|
|
238
260
|
return [...tail];
|
|
239
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* The new lines of a job's tail when the daemon reports its running line count:
|
|
264
|
+
* exactly the last `total − printedCount`, and how many of those already fell
|
|
265
|
+
* out of the window (the daemon keeps the last 200). Unlike overlap matching,
|
|
266
|
+
* this cannot mistake a repeated line for one already printed.
|
|
267
|
+
*/
|
|
268
|
+
function newLogLinesByCount(printedCount, tail, total) {
|
|
269
|
+
const unseen = Math.max(0, total - printedCount);
|
|
270
|
+
if (unseen === 0)
|
|
271
|
+
return { lines: [], skipped: 0 };
|
|
272
|
+
if (unseen > tail.length)
|
|
273
|
+
return { lines: [...tail], skipped: unseen - tail.length };
|
|
274
|
+
return { lines: tail.slice(tail.length - unseen), skipped: 0 };
|
|
275
|
+
}
|
|
240
276
|
/** A 100 MB download over a hotel connection is the case this has to survive. */
|
|
241
277
|
exports.INSTALL_MAX_WAIT_MS = 10 * 60_000;
|
|
242
278
|
exports.INSTALL_POLL_INTERVAL_MS = 2_000;
|
|
@@ -256,6 +292,7 @@ exports.INSTALL_POLL_INTERVAL_MS = 2_000;
|
|
|
256
292
|
async function followManagedCliInstallJob(input) {
|
|
257
293
|
const deadline = Date.now() + (input.maxWaitMs ?? exports.INSTALL_MAX_WAIT_MS);
|
|
258
294
|
let printed = [];
|
|
295
|
+
let printedCount = 0;
|
|
259
296
|
for (;;) {
|
|
260
297
|
const res = await readManagedCliInstallJob(input.daemonId, input.jobId, input.credential);
|
|
261
298
|
const job = res.job;
|
|
@@ -264,7 +301,20 @@ async function followManagedCliInstallJob(input) {
|
|
|
264
301
|
if (input.onLogLine) {
|
|
265
302
|
// `logTail`, the field the daemon sends — this read `log`, which no daemon
|
|
266
303
|
// has ever sent, so every install printed nothing until it ended (OSK-10103).
|
|
267
|
-
const
|
|
304
|
+
const tail = job.logTail ?? job.log ?? [];
|
|
305
|
+
let fresh;
|
|
306
|
+
if (typeof job.logLineCount === 'number') {
|
|
307
|
+
// Exact: the daemon says how many lines exist in total (OSK-10179).
|
|
308
|
+
const { lines, skipped } = newLogLinesByCount(printedCount, tail, job.logLineCount);
|
|
309
|
+
if (skipped > 0) {
|
|
310
|
+
input.onLogLine(`(… ${skipped} log line${skipped === 1 ? '' : 's'} not shown)`);
|
|
311
|
+
}
|
|
312
|
+
fresh = lines;
|
|
313
|
+
printedCount = Math.max(printedCount, job.logLineCount);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
fresh = unseenLogLines(printed, tail);
|
|
317
|
+
}
|
|
268
318
|
for (const line of fresh)
|
|
269
319
|
input.onLogLine(line);
|
|
270
320
|
printed = [...printed, ...fresh].slice(-MAX_TRACKED_LOG_LINES);
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface FollowUpRow {
|
|
|
56
56
|
export interface FollowUpCreateResponse {
|
|
57
57
|
followup: FollowUpRow | null;
|
|
58
58
|
reused?: boolean;
|
|
59
|
+
/** A reused reminder asked for at a different time moved to that time. */
|
|
60
|
+
rescheduled?: boolean;
|
|
59
61
|
dropped?: false | 'budget';
|
|
60
62
|
proposal?: boolean;
|
|
61
63
|
budget?: Json;
|
|
@@ -112,3 +112,30 @@ export declare const SKILL_CONVENTION_DISCOVERY_SINCE = "0.8.44";
|
|
|
112
112
|
* add an explanation, and claiming an unknown daemon is old would be a guess.
|
|
113
113
|
*/
|
|
114
114
|
export declare function daemonPredates(version: string | null | undefined, floor: string): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* The table, summary and exit decision for `skills import-as-actions`.
|
|
117
|
+
*
|
|
118
|
+
* A skill that imported without its action because the library was full used to
|
|
119
|
+
* show only `limit_reached`, an empty detail, "0 skill(s) failed" and exit 0, so
|
|
120
|
+
* a script or agent concluded every action it asked for was added (OSK-10185).
|
|
121
|
+
* `already_exists` stays a success: the action the caller wanted is there.
|
|
122
|
+
*/
|
|
123
|
+
export declare function summarizeSkillActionImport(result: {
|
|
124
|
+
targetAgentId: string;
|
|
125
|
+
actionsCreated: number;
|
|
126
|
+
results: Array<{
|
|
127
|
+
name: string;
|
|
128
|
+
status: string;
|
|
129
|
+
error?: string;
|
|
130
|
+
actionCreated?: boolean;
|
|
131
|
+
actionSkippedReason?: string;
|
|
132
|
+
actionError?: string;
|
|
133
|
+
}>;
|
|
134
|
+
actionsError?: {
|
|
135
|
+
message: string;
|
|
136
|
+
} | null;
|
|
137
|
+
}): {
|
|
138
|
+
rows: Array<Record<string, string>>;
|
|
139
|
+
summary: string;
|
|
140
|
+
failed: boolean;
|
|
141
|
+
};
|
package/dist/lib/local-skills.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.looksLikeSkillPath = looksLikeSkillPath;
|
|
|
10
10
|
exports.findLocalSkillsNamed = findLocalSkillsNamed;
|
|
11
11
|
exports.describeStoreSkillMiss = describeStoreSkillMiss;
|
|
12
12
|
exports.daemonPredates = daemonPredates;
|
|
13
|
+
exports.summarizeSkillActionImport = summarizeSkillActionImport;
|
|
13
14
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
14
15
|
const instruction_input_1 = require("./instruction-input");
|
|
15
16
|
/**
|
|
@@ -211,3 +212,40 @@ function daemonPredates(version, floor) {
|
|
|
211
212
|
}
|
|
212
213
|
return false;
|
|
213
214
|
}
|
|
215
|
+
/** Why a skill imported but got no action, in words a person or an agent can act on. */
|
|
216
|
+
const ACTION_SKIP_DETAIL = {
|
|
217
|
+
limit_reached: "this agent's action library is full; delete an action, then run the import again to add it",
|
|
218
|
+
already_exists: 'an action for this skill is already offered at this scope',
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* The table, summary and exit decision for `skills import-as-actions`.
|
|
222
|
+
*
|
|
223
|
+
* A skill that imported without its action because the library was full used to
|
|
224
|
+
* show only `limit_reached`, an empty detail, "0 skill(s) failed" and exit 0, so
|
|
225
|
+
* a script or agent concluded every action it asked for was added (OSK-10185).
|
|
226
|
+
* `already_exists` stays a success: the action the caller wanted is there.
|
|
227
|
+
*/
|
|
228
|
+
function summarizeSkillActionImport(result) {
|
|
229
|
+
const failed = result.results.filter((r) => r.status === 'failed');
|
|
230
|
+
const overCap = result.results.filter((r) => r.status !== 'failed' && r.actionSkippedReason === 'limit_reached');
|
|
231
|
+
const rows = result.results.map((r) => ({
|
|
232
|
+
skill: r.name,
|
|
233
|
+
import: r.status,
|
|
234
|
+
action: r.actionCreated
|
|
235
|
+
? 'created'
|
|
236
|
+
: (r.actionSkippedReason ?? r.actionError ?? (r.status === 'failed' ? '-' : 'not added')),
|
|
237
|
+
detail: r.error ??
|
|
238
|
+
r.actionError ??
|
|
239
|
+
(r.actionSkippedReason ? (ACTION_SKIP_DETAIL[r.actionSkippedReason] ?? '') : ''),
|
|
240
|
+
}));
|
|
241
|
+
const summary = `${result.actionsCreated} action(s) created on ${result.targetAgentId}; ` +
|
|
242
|
+
`${failed.length} skill(s) failed` +
|
|
243
|
+
(overCap.length > 0
|
|
244
|
+
? `; ${overCap.length} skill(s) imported without an action because the action library is full.`
|
|
245
|
+
: '.');
|
|
246
|
+
return {
|
|
247
|
+
rows,
|
|
248
|
+
summary,
|
|
249
|
+
failed: failed.length > 0 || overCap.length > 0 || Boolean(result.actionsError),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -28,6 +28,19 @@ export type ClosureLike = TaskClosureEvaluation | {
|
|
|
28
28
|
* the whole point is to be able to act without a second lookup.
|
|
29
29
|
*/
|
|
30
30
|
export declare function closureReport(closure: ClosureLike): string[];
|
|
31
|
+
/**
|
|
32
|
+
* The ways forward when a closed Task refuses a Result (`TASK_REOPEN_REQUIRED`).
|
|
33
|
+
*
|
|
34
|
+
* Both writers reach this refusal, `tasks result submit` and `tasks complete`,
|
|
35
|
+
* so both print from here. The first version sat in `result submit` alone and
|
|
36
|
+
* offered `tasks complete` as a way around it, which the same refusal stops:
|
|
37
|
+
* every step named here has to work on a closed Task (OSK-10031).
|
|
38
|
+
*/
|
|
39
|
+
export declare function reopenRequiredLines({ bin, task, command, }: {
|
|
40
|
+
bin: string;
|
|
41
|
+
task: string;
|
|
42
|
+
command: 'result submit' | 'complete';
|
|
43
|
+
}): string[];
|
|
31
44
|
/** `met (agent)` vs `met (person)` — shared with the web through data-provider. */
|
|
32
45
|
export { describeAssessorKind as describeAssessor } from '@skrr-ai/data-provider';
|
|
33
46
|
export { describeClosure as closureSnapshotLines, type TaskClosureSnapshot as ClosureSnapshot, } from '@skrr-ai/data-provider';
|