@scalequality/cli 0.4.8 → 0.4.9-rc.2
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/connect.build.json +2 -2
- package/dist/connect.cjs +282 -45
- package/package.json +1 -1
package/dist/connect.build.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sdkVersion": "0.3.281",
|
|
3
|
-
"sha256": "
|
|
4
|
-
"sourceCommit": "
|
|
3
|
+
"sha256": "f9a1832f530428fd24a48cc17074549aab70faa7ba2f419f3f372321fbe81b0a",
|
|
4
|
+
"sourceCommit": "59a3d8bb1500daa81edb1d977bccf6a459954c5f"
|
|
5
5
|
}
|
package/dist/connect.cjs
CHANGED
|
@@ -2602,6 +2602,28 @@ var ConsoleLog = class {
|
|
|
2602
2602
|
}
|
|
2603
2603
|
}
|
|
2604
2604
|
};
|
|
2605
|
+
function turnTimingLine(ctx) {
|
|
2606
|
+
if (!ctx || ctx.event !== "WORKSPACE_TURN_TIMING") return null;
|
|
2607
|
+
const ms = (k) => typeof ctx[k] === "number" ? ctx[k] : null;
|
|
2608
|
+
const secs = (v) => v >= 1e4 ? `${(v / 1e3).toFixed(1)} s` : `${Math.round(v)} ms`;
|
|
2609
|
+
const parts = [];
|
|
2610
|
+
const first = ms("firstModelEventMs");
|
|
2611
|
+
parts.push(`first model event ${first === null ? "none" : secs(first)}`);
|
|
2612
|
+
const phases = [
|
|
2613
|
+
ms("prepareMs") !== null ? `prepare ${secs(ms("prepareMs"))}` : null,
|
|
2614
|
+
ctx.warmStart === true ? "engine started ahead" : ctx.warmStart === false ? "engine started for it" : null,
|
|
2615
|
+
ms("engineReadyMs") !== null ? `engine ready ${secs(ms("engineReadyMs"))}` : null,
|
|
2616
|
+
ms("checkpointMs") !== null ? `saved point ${secs(ms("checkpointMs"))}` : null,
|
|
2617
|
+
ms("toolWaitMs") !== null ? `tools waited ${secs(ms("toolWaitMs"))}` : null
|
|
2618
|
+
].filter(Boolean);
|
|
2619
|
+
if (phases.length) parts.push(`(${phases.join(", ")})`);
|
|
2620
|
+
const permissions = ms("permissions");
|
|
2621
|
+
if (permissions) parts.push(`; ${permissions} permission${permissions === 1 ? "" : "s"} ${secs(ms("permissionMs") ?? 0)}`);
|
|
2622
|
+
const total = ms("totalMs");
|
|
2623
|
+
if (total !== null) parts.push(`; total ${secs(total)}`);
|
|
2624
|
+
if (typeof ctx.outcome === "string" && ctx.outcome !== "completed") parts.push(`(${ctx.outcome})`);
|
|
2625
|
+
return `Timing: ${parts.join(" ").replace(/ ;/g, ";")}`;
|
|
2626
|
+
}
|
|
2605
2627
|
function banner(boot, local, style2, warnings) {
|
|
2606
2628
|
const model = boot.runtime.primaryModel || boot.model;
|
|
2607
2629
|
const repos = bootScopeRepos(boot);
|
|
@@ -2965,6 +2987,7 @@ function removeStableCopies(home, keep = null) {
|
|
|
2965
2987
|
function serviceArgs(env, bin, api) {
|
|
2966
2988
|
return [env.nodePath, bin, "up", "--api", api, "--service"];
|
|
2967
2989
|
}
|
|
2990
|
+
var LAUNCHD_PROCESS_TYPE = "Interactive";
|
|
2968
2991
|
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2969
2992
|
function launchAgentPlist(env, bin, api) {
|
|
2970
2993
|
const names = serviceNames(api);
|
|
@@ -2990,7 +3013,7 @@ ${args}
|
|
|
2990
3013
|
<key>ThrottleInterval</key>
|
|
2991
3014
|
<integer>10</integer>
|
|
2992
3015
|
<key>ProcessType</key>
|
|
2993
|
-
<string
|
|
3016
|
+
<string>${LAUNCHD_PROCESS_TYPE}</string>
|
|
2994
3017
|
<key>WorkingDirectory</key>
|
|
2995
3018
|
<string>${xml(env.home)}</string>
|
|
2996
3019
|
<key>EnvironmentVariables</key>
|
|
@@ -3042,18 +3065,29 @@ function writeFileSafely(file, content, mode = 420) {
|
|
|
3042
3065
|
(0, import_fs5.renameSync)(tmp, file);
|
|
3043
3066
|
}
|
|
3044
3067
|
var failed = (r) => `${r.stderr || r.stdout}`.trim().split("\n").slice(-1)[0]?.slice(0, 200) || `exit ${r.code}`;
|
|
3068
|
+
var LAUNCHD_REPLACE_WAIT_MS = 3e4;
|
|
3069
|
+
var LAUNCHD_POLL_MS = 500;
|
|
3070
|
+
var LAUNCHD_BOOTSTRAP_ATTEMPTS = 6;
|
|
3045
3071
|
async function installLaunchd(env, bin, api) {
|
|
3046
3072
|
const names = serviceNames(api);
|
|
3047
3073
|
const plist = launchAgentPath(env, api);
|
|
3048
3074
|
(0, import_fs5.mkdirSync)(logDir(env), { recursive: true });
|
|
3049
3075
|
writeFileSafely(plist, launchAgentPlist(env, bin, api));
|
|
3050
3076
|
const domain = `gui/${env.uid}`;
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
const
|
|
3077
|
+
const sleep2 = env.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3078
|
+
const target = `${domain}/${names.label}`;
|
|
3079
|
+
const loaded = async () => (await env.run("launchctl", ["print", target])).code === 0;
|
|
3080
|
+
await env.run("launchctl", ["bootout", target]);
|
|
3081
|
+
for (let waited = 0; waited < LAUNCHD_REPLACE_WAIT_MS && await loaded(); waited += LAUNCHD_POLL_MS) await sleep2(LAUNCHD_POLL_MS);
|
|
3082
|
+
await env.run("launchctl", ["enable", target]);
|
|
3083
|
+
let boot = await env.run("launchctl", ["bootstrap", domain, plist]);
|
|
3084
|
+
for (let attempt = 1; boot.code !== 0 && attempt < LAUNCHD_BOOTSTRAP_ATTEMPTS; attempt++) {
|
|
3085
|
+
await sleep2(LAUNCHD_POLL_MS);
|
|
3086
|
+
boot = await env.run("launchctl", ["bootstrap", domain, plist]);
|
|
3087
|
+
}
|
|
3054
3088
|
if (boot.code === 0) return { ok: true, manager: "launchd", hints: [] };
|
|
3055
3089
|
const legacy = await env.run("launchctl", ["load", "-w", plist]);
|
|
3056
|
-
if (legacy.code === 0) return { ok: true, manager: "launchd", hints: [] };
|
|
3090
|
+
if (legacy.code === 0 && await loaded()) return { ok: true, manager: "launchd", hints: [] };
|
|
3057
3091
|
(0, import_fs5.rmSync)(plist, { force: true });
|
|
3058
3092
|
return { ok: false, code: "LAUNCHD_REFUSED", message: `macOS did not accept the background service (${failed(boot)}). A device management profile may block login items.` };
|
|
3059
3093
|
}
|
|
@@ -4411,7 +4445,7 @@ function limitInEngineText(text3) {
|
|
|
4411
4445
|
// src/application/services/workspaceSandbox/scalequalityTools.ts
|
|
4412
4446
|
var import_crypto4 = require("crypto");
|
|
4413
4447
|
|
|
4414
|
-
//
|
|
4448
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/external.js
|
|
4415
4449
|
var external_exports = {};
|
|
4416
4450
|
__export(external_exports, {
|
|
4417
4451
|
BRAND: () => BRAND,
|
|
@@ -4523,7 +4557,7 @@ __export(external_exports, {
|
|
|
4523
4557
|
void: () => voidType
|
|
4524
4558
|
});
|
|
4525
4559
|
|
|
4526
|
-
//
|
|
4560
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/util.js
|
|
4527
4561
|
var util;
|
|
4528
4562
|
(function(util2) {
|
|
4529
4563
|
util2.assertEqual = (_) => {
|
|
@@ -4657,7 +4691,7 @@ var getParsedType = (data) => {
|
|
|
4657
4691
|
}
|
|
4658
4692
|
};
|
|
4659
4693
|
|
|
4660
|
-
//
|
|
4694
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/ZodError.js
|
|
4661
4695
|
var ZodIssueCode = util.arrayToEnum([
|
|
4662
4696
|
"invalid_type",
|
|
4663
4697
|
"invalid_literal",
|
|
@@ -4775,7 +4809,7 @@ ZodError.create = (issues) => {
|
|
|
4775
4809
|
return error;
|
|
4776
4810
|
};
|
|
4777
4811
|
|
|
4778
|
-
//
|
|
4812
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/locales/en.js
|
|
4779
4813
|
var errorMap = (issue, _ctx) => {
|
|
4780
4814
|
let message;
|
|
4781
4815
|
switch (issue.code) {
|
|
@@ -4878,7 +4912,7 @@ var errorMap = (issue, _ctx) => {
|
|
|
4878
4912
|
};
|
|
4879
4913
|
var en_default = errorMap;
|
|
4880
4914
|
|
|
4881
|
-
//
|
|
4915
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/errors.js
|
|
4882
4916
|
var overrideErrorMap = en_default;
|
|
4883
4917
|
function setErrorMap(map) {
|
|
4884
4918
|
overrideErrorMap = map;
|
|
@@ -4887,7 +4921,7 @@ function getErrorMap() {
|
|
|
4887
4921
|
return overrideErrorMap;
|
|
4888
4922
|
}
|
|
4889
4923
|
|
|
4890
|
-
//
|
|
4924
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/parseUtil.js
|
|
4891
4925
|
var makeIssue = (params) => {
|
|
4892
4926
|
const { data, path, errorMaps, issueData } = params;
|
|
4893
4927
|
const fullPath = [...path, ...issueData.path || []];
|
|
@@ -4997,14 +5031,14 @@ var isDirty = (x) => x.status === "dirty";
|
|
|
4997
5031
|
var isValid = (x) => x.status === "valid";
|
|
4998
5032
|
var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
4999
5033
|
|
|
5000
|
-
//
|
|
5034
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/errorUtil.js
|
|
5001
5035
|
var errorUtil;
|
|
5002
5036
|
(function(errorUtil2) {
|
|
5003
5037
|
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
|
|
5004
5038
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
|
|
5005
5039
|
})(errorUtil || (errorUtil = {}));
|
|
5006
5040
|
|
|
5007
|
-
//
|
|
5041
|
+
// ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/types.js
|
|
5008
5042
|
var ParseInputLazyPath = class {
|
|
5009
5043
|
constructor(parent, value, path, key) {
|
|
5010
5044
|
this._cachedPath = [];
|
|
@@ -9164,6 +9198,28 @@ var TurnTiming = class {
|
|
|
9164
9198
|
calls = 0;
|
|
9165
9199
|
answers = /* @__PURE__ */ new Set();
|
|
9166
9200
|
done = false;
|
|
9201
|
+
firstFrame = null;
|
|
9202
|
+
phase = {};
|
|
9203
|
+
permissionTotal = 0;
|
|
9204
|
+
permissionCount = 0;
|
|
9205
|
+
toolWaitTotal = 0;
|
|
9206
|
+
/** Durations (ms) and flags of the turn's own phases. */
|
|
9207
|
+
phases(p) {
|
|
9208
|
+
Object.assign(this.phase, p);
|
|
9209
|
+
}
|
|
9210
|
+
/** Any frame from the engine process (the first one says it is up and has the message). */
|
|
9211
|
+
engineFrame() {
|
|
9212
|
+
if (this.firstFrame === null) this.firstFrame = this.now();
|
|
9213
|
+
}
|
|
9214
|
+
/** A command or change permission the engine asked for (the rules, the person's mode or the person answered). */
|
|
9215
|
+
permission(ms) {
|
|
9216
|
+
this.permissionTotal += ms;
|
|
9217
|
+
this.permissionCount += 1;
|
|
9218
|
+
}
|
|
9219
|
+
/** A tool waited for the turn's saved point. */
|
|
9220
|
+
toolWait(ms) {
|
|
9221
|
+
this.toolWaitTotal += ms;
|
|
9222
|
+
}
|
|
9167
9223
|
/** Any frame of the main conversation that came from the model. */
|
|
9168
9224
|
modelEvent() {
|
|
9169
9225
|
if (this.firstModel === null) this.firstModel = this.now();
|
|
@@ -9200,6 +9256,10 @@ var TurnTiming = class {
|
|
|
9200
9256
|
firstVisibleEventMs: since(this.firstVisible),
|
|
9201
9257
|
totalMs: end - this.startedAt,
|
|
9202
9258
|
modelCalls: this.calls,
|
|
9259
|
+
...this.phase,
|
|
9260
|
+
engineReadyMs: since(this.firstFrame),
|
|
9261
|
+
...this.permissionCount ? { permissions: this.permissionCount, permissionMs: this.permissionTotal } : {},
|
|
9262
|
+
...this.toolWaitTotal ? { toolWaitMs: this.toolWaitTotal } : {},
|
|
9203
9263
|
outcome,
|
|
9204
9264
|
...extra
|
|
9205
9265
|
});
|
|
@@ -9836,6 +9896,7 @@ ${lines2.join("\n")}
|
|
|
9836
9896
|
}
|
|
9837
9897
|
|
|
9838
9898
|
// src/application/services/workspaceSandbox/WorkspaceEngine.ts
|
|
9899
|
+
var WARM_IDLE_MS = 15 * 6e4;
|
|
9839
9900
|
var STOP_GRACE_MS = 8e3;
|
|
9840
9901
|
var INTERRUPT_TIMEOUT_MS = 3e3;
|
|
9841
9902
|
var MODEL_TOOLS = ["Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep", "Bash", "TaskStop", "TodoWrite", "Agent", "WebFetch", "Skill"];
|
|
@@ -10032,6 +10093,14 @@ var WorkspaceEngine = class {
|
|
|
10032
10093
|
envTrouble = [];
|
|
10033
10094
|
/** The organization's approved MCP servers: their catalog and the in-process servers that forward to the API. */
|
|
10034
10095
|
orgMcp = null;
|
|
10096
|
+
/**
|
|
10097
|
+
* The saved point of the turn the engine runs now: every tool waits for it (the PreToolUse hook and the permission
|
|
10098
|
+
* prompt), so the engine process starts while it is taken and no tool changes a file before it is recorded.
|
|
10099
|
+
*/
|
|
10100
|
+
toolGate = Promise.resolve();
|
|
10101
|
+
/** The engine process started for the next turn (warmStart), and the start options it was given (their hash). */
|
|
10102
|
+
warm = null;
|
|
10103
|
+
prewarming = null;
|
|
10035
10104
|
emit(e) {
|
|
10036
10105
|
if (e.type === "terminal" && e.data.chunk === void 0 && this.folderBaseline) this.sweepPending = true;
|
|
10037
10106
|
if (this.turnWatch) {
|
|
@@ -10221,6 +10290,7 @@ var WorkspaceEngine = class {
|
|
|
10221
10290
|
if (this.resumedFromCheckpoint) await this.diffNow();
|
|
10222
10291
|
this.phase("READY");
|
|
10223
10292
|
await this.sink.flush();
|
|
10293
|
+
this.prewarm();
|
|
10224
10294
|
return true;
|
|
10225
10295
|
}
|
|
10226
10296
|
/** Where a background command writes its output: inside the config dir, which Read may read. */
|
|
@@ -10556,6 +10626,7 @@ var WorkspaceEngine = class {
|
|
|
10556
10626
|
} finally {
|
|
10557
10627
|
this.turnRunning = null;
|
|
10558
10628
|
}
|
|
10629
|
+
this.prewarm();
|
|
10559
10630
|
})();
|
|
10560
10631
|
}
|
|
10561
10632
|
/** Waits for the current turn (test and shutdown helper). */
|
|
@@ -10839,14 +10910,16 @@ ${String(item.content)}` : String(item.content);
|
|
|
10839
10910
|
this.recordCheckpoint(turnId, await this.takeSnapshot());
|
|
10840
10911
|
}
|
|
10841
10912
|
async takeSnapshot() {
|
|
10913
|
+
const sdkSessionId = this.sdkSessionId;
|
|
10914
|
+
const resumeAt = this.resumeAt ?? this.lastChainUuid;
|
|
10842
10915
|
const trees = /* @__PURE__ */ new Map();
|
|
10843
|
-
await this.folderBaseline;
|
|
10916
|
+
await this.folderBaseline?.catch(() => void 0);
|
|
10844
10917
|
for (const repo2 of this.repos.values()) {
|
|
10845
10918
|
if (repo2.dormant) continue;
|
|
10846
10919
|
const tree = await repo2.tracker.snapshot().catch(() => null);
|
|
10847
10920
|
if (tree) trees.set(repo2.root, tree);
|
|
10848
10921
|
}
|
|
10849
|
-
return { trees, sdkSessionId
|
|
10922
|
+
return { trees, sdkSessionId, resumeAt };
|
|
10850
10923
|
}
|
|
10851
10924
|
recordCheckpoint(turnId, snap) {
|
|
10852
10925
|
const at = iso2();
|
|
@@ -11220,9 +11293,16 @@ ${json}
|
|
|
11220
11293
|
this.turnWatch = { lastExit: null, errored: false };
|
|
11221
11294
|
if (!compact && typeof payload.turnId === "string" && payload.turnId) this.messageStatus(turnId, "started");
|
|
11222
11295
|
this.setState("WORKING");
|
|
11223
|
-
|
|
11296
|
+
let checkpointMs = null;
|
|
11297
|
+
const saved = compact ? Promise.resolve() : this.checkpointTurn(turnId).then(() => {
|
|
11298
|
+
checkpointMs = Date.now() - turnStart;
|
|
11299
|
+
}, (e) => {
|
|
11300
|
+
this.deps.log.warn("turn checkpoint failed", { error: this.redactor.text(e.message) });
|
|
11301
|
+
});
|
|
11302
|
+
this.toolGate = saved;
|
|
11224
11303
|
if (!compact) {
|
|
11225
11304
|
await this.refreshSkills();
|
|
11305
|
+
if (!this.local) await saved;
|
|
11226
11306
|
this.startEnvSetups();
|
|
11227
11307
|
await Promise.all(this.envSetups.values());
|
|
11228
11308
|
}
|
|
@@ -11320,6 +11400,9 @@ ${text3}`;
|
|
|
11320
11400
|
timing: new TurnTiming({ sessionId: this.deps.sessionId ?? null, turnId, model, reasoning: this.reasoningLevel }, typeof payload.receivedAt === "number" ? payload.receivedAt : null, turnStart)
|
|
11321
11401
|
};
|
|
11322
11402
|
this.live = live;
|
|
11403
|
+
void saved.then(() => {
|
|
11404
|
+
if (checkpointMs !== null) live.timing.phases({ checkpointMs });
|
|
11405
|
+
});
|
|
11323
11406
|
const attempt = async (resume, at) => {
|
|
11324
11407
|
let sawInit = false;
|
|
11325
11408
|
let conversation = resume;
|
|
@@ -11356,41 +11439,26 @@ ${text3}`;
|
|
|
11356
11439
|
live.turnEnded = false;
|
|
11357
11440
|
live.headResult = null;
|
|
11358
11441
|
channel.start(userMessage(turnPrompt, context.images, messageUuid(turnId)));
|
|
11359
|
-
const
|
|
11360
|
-
root: this.deps.root,
|
|
11361
|
-
model,
|
|
11362
|
-
resume,
|
|
11363
|
-
resumeAt: resume ? at : null,
|
|
11364
|
-
abortController: ac,
|
|
11365
|
-
reasoning: reasoning.options,
|
|
11366
|
-
env: buildEngineEnv(boot, this.deps.configDir, model, { local: this.local, reasoning, output, extraEnv: this.projectEnv(), plan: planMode }),
|
|
11367
|
-
mcpServer: this.mcpServer,
|
|
11368
|
-
systemAppend: await this.systemAppend(),
|
|
11369
|
-
policy: {
|
|
11370
|
-
root: this.deps.root,
|
|
11371
|
-
extraReadRoots: [this.deps.configDir],
|
|
11372
|
-
deniedRoots: this.deps.privateDirs,
|
|
11373
|
-
local: this.local,
|
|
11374
|
-
plan: planMode,
|
|
11375
|
-
planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
|
|
11376
|
-
orgMcpTools: orgMcpToolNames(Object.keys(off.mcpServers).length ? off.mcpCatalog : null)
|
|
11377
|
-
},
|
|
11378
|
-
plan: planMode,
|
|
11379
|
-
onPlan: (text3) => this.proposePlan(live.turnId, text3),
|
|
11380
|
-
skills: { dir: this.skillsDir(), enabled: enabledSkillNames(off.skillEntries), pluginDirs: off.pluginDirs },
|
|
11381
|
-
...Object.keys(off.mcpServers).length ? { orgMcpServers: off.mcpServers } : {},
|
|
11382
|
-
pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
|
|
11383
|
-
commandGate: this.deps.commandGate,
|
|
11384
|
-
...this.localFolder?.kind === "folder" ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
|
|
11385
|
-
});
|
|
11442
|
+
const inputs = await this.engineInputs({ model, reasoning, output, planMode, off, resume, resumeAt: resume ? at : null });
|
|
11386
11443
|
if (!streaming) channel.close();
|
|
11387
11444
|
const single = (text3) => context.images.length ? (async function* () {
|
|
11388
11445
|
yield userMessage(text3, context.images);
|
|
11389
11446
|
})() : text3;
|
|
11390
|
-
const
|
|
11447
|
+
const warm = streaming ? await this.claimWarm(spawnKey(inputs)) : (this.dropWarm(), null);
|
|
11448
|
+
let q;
|
|
11449
|
+
if (warm) {
|
|
11450
|
+
const forward = () => warm.abort.abort();
|
|
11451
|
+
if (ac.signal.aborted) forward();
|
|
11452
|
+
else ac.signal.addEventListener("abort", forward, { once: true });
|
|
11453
|
+
q = warm.handle.query(channel);
|
|
11454
|
+
} else {
|
|
11455
|
+
q = sdk.query({ prompt: streaming ? channel : single(turnPrompt), options: this.queryOptions(inputs, ac, this.mcpServer) });
|
|
11456
|
+
}
|
|
11457
|
+
live.timing.phases({ prepareMs: Date.now() - turnStart, warmStart: !!warm });
|
|
11391
11458
|
live.query = q;
|
|
11392
11459
|
try {
|
|
11393
11460
|
for await (const msg of q) {
|
|
11461
|
+
live.timing.engineFrame();
|
|
11394
11462
|
const f = msg && typeof msg === "object" ? msg : {};
|
|
11395
11463
|
const main2 = !f.parent_tool_use_id;
|
|
11396
11464
|
if (f.type === "command_lifecycle") {
|
|
@@ -11445,6 +11513,7 @@ ${text3}`;
|
|
|
11445
11513
|
}
|
|
11446
11514
|
} finally {
|
|
11447
11515
|
const stopped = ac.signal.aborted || live.interrupted;
|
|
11516
|
+
await saved;
|
|
11448
11517
|
live.timing.end(this.deps.log, stopped ? "stopped" : "failed");
|
|
11449
11518
|
this.live = null;
|
|
11450
11519
|
this.turnAbort = null;
|
|
@@ -11585,6 +11654,7 @@ ${text3}`;
|
|
|
11585
11654
|
}
|
|
11586
11655
|
live.between = true;
|
|
11587
11656
|
live.snapshot = this.takeSnapshot();
|
|
11657
|
+
this.toolGate = live.snapshot.then(() => void 0, () => void 0);
|
|
11588
11658
|
}
|
|
11589
11659
|
/** The query ended: its input closes, and a message it never read runs as the next turn (or is dropped by a rewind). */
|
|
11590
11660
|
settleLive(live) {
|
|
@@ -11625,6 +11695,146 @@ ${text3}`;
|
|
|
11625
11695
|
output: { limit: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null, ceiling: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null }
|
|
11626
11696
|
};
|
|
11627
11697
|
}
|
|
11698
|
+
/** The data a turn's engine process starts with (see EngineInputs). */
|
|
11699
|
+
async engineInputs(t) {
|
|
11700
|
+
const boot = this.boot;
|
|
11701
|
+
const org = Object.keys(t.off.mcpServers).length > 0;
|
|
11702
|
+
return {
|
|
11703
|
+
model: t.model,
|
|
11704
|
+
resume: t.resume,
|
|
11705
|
+
resumeAt: t.resumeAt,
|
|
11706
|
+
reasoning: t.reasoning.options,
|
|
11707
|
+
env: buildEngineEnv(boot, this.deps.configDir, t.model, { local: this.local, reasoning: t.reasoning, output: t.output, extraEnv: this.projectEnv(), plan: t.planMode }),
|
|
11708
|
+
systemAppend: await this.systemAppend(),
|
|
11709
|
+
policy: {
|
|
11710
|
+
root: this.deps.root,
|
|
11711
|
+
extraReadRoots: [this.deps.configDir],
|
|
11712
|
+
deniedRoots: this.deps.privateDirs,
|
|
11713
|
+
local: this.local,
|
|
11714
|
+
plan: t.planMode,
|
|
11715
|
+
planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
|
|
11716
|
+
orgMcpTools: orgMcpToolNames(org ? t.off.mcpCatalog : null)
|
|
11717
|
+
},
|
|
11718
|
+
plan: t.planMode,
|
|
11719
|
+
skills: { dir: this.skillsDir(), enabled: enabledSkillNames(t.off.skillEntries), pluginDirs: t.off.pluginDirs },
|
|
11720
|
+
skillsKey: this.skillsSent,
|
|
11721
|
+
orgMcpServers: org ? t.off.mcpServers : {},
|
|
11722
|
+
folderWrites: this.localFolder?.kind === "folder"
|
|
11723
|
+
};
|
|
11724
|
+
}
|
|
11725
|
+
/**
|
|
11726
|
+
* The query options of a turn's engine process. The callbacks read the engine's current turn (never one captured
|
|
11727
|
+
* here), so a process started ahead serves the turn that takes it: its plan goes to that turn, and its tools wait
|
|
11728
|
+
* for that turn's saved point.
|
|
11729
|
+
*/
|
|
11730
|
+
queryOptions(i, abortController, mcpServer) {
|
|
11731
|
+
const gate = this.deps.commandGate;
|
|
11732
|
+
return buildQueryOptions({
|
|
11733
|
+
root: this.deps.root,
|
|
11734
|
+
model: i.model,
|
|
11735
|
+
resume: i.resume,
|
|
11736
|
+
resumeAt: i.resumeAt,
|
|
11737
|
+
abortController,
|
|
11738
|
+
reasoning: i.reasoning,
|
|
11739
|
+
env: i.env,
|
|
11740
|
+
mcpServer,
|
|
11741
|
+
systemAppend: i.systemAppend,
|
|
11742
|
+
policy: i.policy,
|
|
11743
|
+
plan: i.plan,
|
|
11744
|
+
onPlan: (text3) => {
|
|
11745
|
+
const live = this.live;
|
|
11746
|
+
if (live) this.proposePlan(live.turnId, text3);
|
|
11747
|
+
},
|
|
11748
|
+
skills: i.skills,
|
|
11749
|
+
...Object.keys(i.orgMcpServers).length ? { orgMcpServers: i.orgMcpServers } : {},
|
|
11750
|
+
pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
|
|
11751
|
+
commandGate: gate ? timedGate(gate, (ms) => this.live?.timing.permission(ms)) : void 0,
|
|
11752
|
+
beforeTool: async () => {
|
|
11753
|
+
const t0 = Date.now();
|
|
11754
|
+
await this.toolGate;
|
|
11755
|
+
const waited = Date.now() - t0;
|
|
11756
|
+
if (waited > 0) this.live?.timing.toolWait(waited);
|
|
11757
|
+
},
|
|
11758
|
+
...i.folderWrites ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
|
|
11759
|
+
});
|
|
11760
|
+
}
|
|
11761
|
+
/**
|
|
11762
|
+
* The inputs of the next turn as the person would most likely send it (same model, reasoning, mode and tools as the
|
|
11763
|
+
* session now, an everyday task), for the process started ahead. null when that process could not serve it safely:
|
|
11764
|
+
* the organization's MCP servers are in-process objects shared by every process, and one serves one process at a time.
|
|
11765
|
+
*/
|
|
11766
|
+
async nextTurnInputs() {
|
|
11767
|
+
const boot = this.boot;
|
|
11768
|
+
const off = this.turnOff(void 0);
|
|
11769
|
+
if (Object.keys(off.mcpServers).length) return null;
|
|
11770
|
+
const primary = boot.runtime.primaryModel || boot.model;
|
|
11771
|
+
const route = routeAutoTurn(boot, {
|
|
11772
|
+
content: "",
|
|
11773
|
+
previousTrouble: this.previousTrouble,
|
|
11774
|
+
maxMode: this.reasoningLevel === "max" || isMaxMode(this.reasoningLevel, this.reasoningCapability)
|
|
11775
|
+
});
|
|
11776
|
+
const { model, reasoning, output } = this.turnModel(primary, route);
|
|
11777
|
+
const canResume = !!this.sdkSessionId && (this.knownSessions.has(this.sdkSessionId) || await hasLocalTranscript(this.deps.configDir, this.sdkSessionId));
|
|
11778
|
+
return this.engineInputs({ model, reasoning, output, planMode: false, off, resume: canResume ? this.sdkSessionId : null, resumeAt: canResume ? this.resumeAt : null });
|
|
11779
|
+
}
|
|
11780
|
+
/**
|
|
11781
|
+
* Starts the engine process of the next turn now (warmStart), while the session waits for the person: Claude Code
|
|
11782
|
+
* keeps its process up between messages, and here the next message then goes to a ready process. Only when nothing
|
|
11783
|
+
* runs or waits; a process already started with the same inputs is kept.
|
|
11784
|
+
*/
|
|
11785
|
+
prewarm() {
|
|
11786
|
+
const sdk = this.sdk;
|
|
11787
|
+
if (!this.deps.warmStart || !sdk?.startup || !this.boot || this.stopping || this.turnRunning || this.queue.length || this.prewarming) return;
|
|
11788
|
+
this.prewarming = (async () => {
|
|
11789
|
+
const inputs = await this.nextTurnInputs();
|
|
11790
|
+
if (!inputs || this.stopping || this.turnRunning || this.queue.length) return;
|
|
11791
|
+
const key = spawnKey(inputs);
|
|
11792
|
+
if (this.warm?.key === key) return;
|
|
11793
|
+
this.dropWarm();
|
|
11794
|
+
const abort = new AbortController();
|
|
11795
|
+
const server = buildScaleQualityServer(sdk, this.toolHost());
|
|
11796
|
+
const ready = sdk.startup({ options: this.queryOptions(inputs, abort, server) }).catch((e) => {
|
|
11797
|
+
this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
|
|
11798
|
+
return null;
|
|
11799
|
+
});
|
|
11800
|
+
const timer = setTimeout(() => {
|
|
11801
|
+
if (this.warm?.abort === abort) this.dropWarm();
|
|
11802
|
+
}, this.deps.warmIdleMs ?? WARM_IDLE_MS);
|
|
11803
|
+
timer.unref?.();
|
|
11804
|
+
this.warm = { key, abort, ready, timer };
|
|
11805
|
+
})().catch((e) => {
|
|
11806
|
+
this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
|
|
11807
|
+
}).finally(() => {
|
|
11808
|
+
this.prewarming = null;
|
|
11809
|
+
});
|
|
11810
|
+
}
|
|
11811
|
+
/** The process started ahead, for a turn with this start key; any other is closed (the turn starts its own). */
|
|
11812
|
+
async claimWarm(key) {
|
|
11813
|
+
await this.prewarming;
|
|
11814
|
+
const w = this.warm;
|
|
11815
|
+
if (!w) return null;
|
|
11816
|
+
if (w.key !== key) {
|
|
11817
|
+
this.dropWarm();
|
|
11818
|
+
return null;
|
|
11819
|
+
}
|
|
11820
|
+
this.warm = null;
|
|
11821
|
+
clearTimeout(w.timer);
|
|
11822
|
+
const handle = await w.ready;
|
|
11823
|
+
return handle ? { handle, abort: w.abort } : null;
|
|
11824
|
+
}
|
|
11825
|
+
/** Closes the process started ahead, if any. */
|
|
11826
|
+
dropWarm() {
|
|
11827
|
+
const w = this.warm;
|
|
11828
|
+
if (!w) return;
|
|
11829
|
+
this.warm = null;
|
|
11830
|
+
clearTimeout(w.timer);
|
|
11831
|
+
void w.ready.then((h) => {
|
|
11832
|
+
try {
|
|
11833
|
+
h?.close();
|
|
11834
|
+
} catch {
|
|
11835
|
+
}
|
|
11836
|
+
}).finally(() => w.abort.abort());
|
|
11837
|
+
}
|
|
11628
11838
|
/** The imported conversation as a context block (fetched once); null when there is none or it cannot be read. */
|
|
11629
11839
|
importedHistoryBlock() {
|
|
11630
11840
|
const info = this.boot?.imported;
|
|
@@ -11736,6 +11946,7 @@ ${patch}`;
|
|
|
11736
11946
|
if (this.stopping) return;
|
|
11737
11947
|
this.stopping = true;
|
|
11738
11948
|
this.queue.length = 0;
|
|
11949
|
+
this.dropWarm();
|
|
11739
11950
|
this.turnAbort?.abort();
|
|
11740
11951
|
this.pollAbort.abort();
|
|
11741
11952
|
this.envAbort.abort();
|
|
@@ -12261,6 +12472,7 @@ function buildQueryOptions(o) {
|
|
|
12261
12472
|
};
|
|
12262
12473
|
const preToolUse = async (input) => {
|
|
12263
12474
|
if (input.hook_event_name !== "PreToolUse") return {};
|
|
12475
|
+
if (o.beforeTool) await o.beforeTool();
|
|
12264
12476
|
const name = String(input.tool_name ?? "");
|
|
12265
12477
|
if (name === "ExitPlanMode") return o.plan ? {} : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Not in plan mode." } };
|
|
12266
12478
|
const provenance = input.mcp_server;
|
|
@@ -12276,6 +12488,7 @@ function buildQueryOptions(o) {
|
|
|
12276
12488
|
return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
|
|
12277
12489
|
};
|
|
12278
12490
|
const canUseTool = async (toolName, input, opts) => {
|
|
12491
|
+
if (o.beforeTool) await o.beforeTool();
|
|
12279
12492
|
if (toolName.startsWith("mcp__") && opts?.mcpServer?.source && opts.mcpServer.source !== "sdk") {
|
|
12280
12493
|
return { behavior: "deny", message: "Only ScaleQuality tools are available in this workspace." };
|
|
12281
12494
|
}
|
|
@@ -12337,6 +12550,24 @@ function buildQueryOptions(o) {
|
|
|
12337
12550
|
...o.pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable: o.pathToClaudeCodeExecutable } : {}
|
|
12338
12551
|
};
|
|
12339
12552
|
}
|
|
12553
|
+
function spawnKey(i) {
|
|
12554
|
+
const { orgMcpServers, ...data } = i;
|
|
12555
|
+
return (0, import_crypto7.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
|
|
12556
|
+
}
|
|
12557
|
+
function timedGate(gate, took) {
|
|
12558
|
+
const timed = async (run2) => {
|
|
12559
|
+
const t0 = Date.now();
|
|
12560
|
+
try {
|
|
12561
|
+
return await run2();
|
|
12562
|
+
} finally {
|
|
12563
|
+
took(Date.now() - t0);
|
|
12564
|
+
}
|
|
12565
|
+
};
|
|
12566
|
+
return {
|
|
12567
|
+
check: (command, opts) => timed(() => gate.check(command, opts)),
|
|
12568
|
+
...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {}
|
|
12569
|
+
};
|
|
12570
|
+
}
|
|
12340
12571
|
async function hasLocalTranscript(configDir, sessionId) {
|
|
12341
12572
|
if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return false;
|
|
12342
12573
|
const projects = (0, import_path15.join)(configDir, "projects");
|
|
@@ -12390,6 +12621,10 @@ function createLocalEngine(o) {
|
|
|
12390
12621
|
const log = {
|
|
12391
12622
|
info: (msg, ctx) => {
|
|
12392
12623
|
if (o.verbose) say(style.dim(`${o.prefix ?? ""}[info] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
|
|
12624
|
+
else if (msg === "WORKSPACE_TURN_TIMING") {
|
|
12625
|
+
const line = turnTimingLine(ctx);
|
|
12626
|
+
if (line) say(`${o.prefix ?? ""}${style.dim(` ${line}`)}`);
|
|
12627
|
+
}
|
|
12393
12628
|
},
|
|
12394
12629
|
warn: (msg, ctx) => {
|
|
12395
12630
|
if (o.verbose) say(style.dim(`${o.prefix ?? ""}[warn] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
|
|
@@ -12465,7 +12700,9 @@ function createLocalEngine(o) {
|
|
|
12465
12700
|
// An imported Claude Code conversation of this computer and folder resumes from its own transcript.
|
|
12466
12701
|
resumeImported: (externalId) => copyClaudeTranscript(sources, externalId, o.root, configDir),
|
|
12467
12702
|
exit: (code, reason) => o.exit(code, reason, startError),
|
|
12468
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0
|
|
12703
|
+
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0,
|
|
12704
|
+
// The next message's engine process is started while the person reads, like Claude Code's process between messages.
|
|
12705
|
+
warmStart: true
|
|
12469
12706
|
});
|
|
12470
12707
|
}
|
|
12471
12708
|
async function connectMain(argv) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalequality/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9-rc.2",
|
|
4
4
|
"description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|