@mtreeai/msapling-cli 2.3.6-beta.44 → 2.3.6-beta.46
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/index.js +520 -30
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1017,6 +1017,21 @@ var init_src = __esm({
|
|
|
1017
1017
|
const data = overviewData;
|
|
1018
1018
|
return this.mapUser(data, billingData);
|
|
1019
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* CLI-PARITY-P0-3: Web search via the backend proxy (POST /api/web/search).
|
|
1022
|
+
*
|
|
1023
|
+
* Provider API keys (Tavily/Serper) stay server-side; the CLI only ever sees
|
|
1024
|
+
* normalised result rows. Mirrors the WebSearchTool surface so the agent can
|
|
1025
|
+
* search the live web without local credentials. Backend fails open with an
|
|
1026
|
+
* empty result set + provider "none" rather than 500 when no provider is
|
|
1027
|
+
* available, so callers can degrade gracefully.
|
|
1028
|
+
*/
|
|
1029
|
+
async webSearch(query, maxResults = 5) {
|
|
1030
|
+
return await this.request("/api/web/search", {
|
|
1031
|
+
method: "POST",
|
|
1032
|
+
body: JSON.stringify({ query, max_results: maxResults })
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1020
1035
|
async getHistory(chatId) {
|
|
1021
1036
|
const data = await this.request(`/api/projects/chat/${chatId}/history`);
|
|
1022
1037
|
return data.messages.map((m) => ({
|
|
@@ -1268,6 +1283,30 @@ var init_src = __esm({
|
|
|
1268
1283
|
const data = await this.request(`/api/projects/${encodeURIComponent(projectId)}/chats`);
|
|
1269
1284
|
return Array.isArray(data) ? data : data.chats ?? [];
|
|
1270
1285
|
}
|
|
1286
|
+
// P0-4 (CLI-RESUME-01): list the user's recent chats across ALL projects for
|
|
1287
|
+
// `/resume` + `msapling --continue`. The /api/projects/ tree keys projects by
|
|
1288
|
+
// NAME and nests each project's `chats` array ({id, title, model, created_at,
|
|
1289
|
+
// updated_at?}). We flatten the tree, stamp each chat with its project name,
|
|
1290
|
+
// and sort by recency (updated_at, then created_at) so the most-recently
|
|
1291
|
+
// touched chat is first. Backend doesn't expose a flat "recent chats" route,
|
|
1292
|
+
// so this client-side flatten is the canonical path (mirrors chat.ts).
|
|
1293
|
+
async listRecentChats(limit = 20) {
|
|
1294
|
+
const data = await this.request("/api/projects/");
|
|
1295
|
+
const flat = [];
|
|
1296
|
+
for (const [projectName, bucket] of Object.entries(data.projects ?? {})) {
|
|
1297
|
+
for (const chat of bucket?.chats ?? []) {
|
|
1298
|
+
if (!chat?.id) continue;
|
|
1299
|
+
flat.push({ ...chat, project: projectName });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const recencyKey = (c) => {
|
|
1303
|
+
const raw = c.updated_at ?? c.created_at;
|
|
1304
|
+
const t = raw ? Date.parse(raw) : NaN;
|
|
1305
|
+
return Number.isNaN(t) ? 0 : t;
|
|
1306
|
+
};
|
|
1307
|
+
flat.sort((a, b) => recencyKey(b) - recencyKey(a));
|
|
1308
|
+
return flat.slice(0, Math.max(1, limit));
|
|
1309
|
+
}
|
|
1271
1310
|
// CLI-CHAT-CREATE-01 (Iter 34): create a new chat in a project. Per LAB
|
|
1272
1311
|
// projects.py:365 — POST /api/projects/chat/new with
|
|
1273
1312
|
// {project_name, slot_label?, chat_name?, model?, client_type?}.
|
|
@@ -3400,11 +3439,20 @@ var init_DispatchAgentTool = __esm({
|
|
|
3400
3439
|
client;
|
|
3401
3440
|
parentChatId;
|
|
3402
3441
|
projectRoot;
|
|
3442
|
+
onSubagentStop;
|
|
3403
3443
|
constructor(options) {
|
|
3404
3444
|
super();
|
|
3405
3445
|
this.client = options.client;
|
|
3406
3446
|
this.parentChatId = options.parentChatId ?? void 0;
|
|
3407
3447
|
this.projectRoot = options.projectRoot;
|
|
3448
|
+
this.onSubagentStop = options.onSubagentStop;
|
|
3449
|
+
}
|
|
3450
|
+
/** Fire the subagent-stop callback defensively — it must never throw. */
|
|
3451
|
+
signalStop(info) {
|
|
3452
|
+
try {
|
|
3453
|
+
this.onSubagentStop?.(info);
|
|
3454
|
+
} catch {
|
|
3455
|
+
}
|
|
3408
3456
|
}
|
|
3409
3457
|
async execute(args2, _projectRoot) {
|
|
3410
3458
|
if (!args2?.prompt || typeof args2.prompt !== "string" || args2.prompt.trim() === "") {
|
|
@@ -3444,12 +3492,14 @@ var init_DispatchAgentTool = __esm({
|
|
|
3444
3492
|
try {
|
|
3445
3493
|
await Promise.race([streamPromise, timeoutPromise]);
|
|
3446
3494
|
} catch (e) {
|
|
3495
|
+
this.signalStop({ description, ok: false, chars: response.length, error: e?.message });
|
|
3447
3496
|
return {
|
|
3448
3497
|
content: `dispatch_agent error [${description}]: ${e.message}`,
|
|
3449
3498
|
isError: true
|
|
3450
3499
|
};
|
|
3451
3500
|
}
|
|
3452
3501
|
if (!response.trim()) {
|
|
3502
|
+
this.signalStop({ description, ok: false, chars: 0, error: "empty response" });
|
|
3453
3503
|
return {
|
|
3454
3504
|
content: `dispatch_agent [${description}]: Sub-agent returned an empty response.`,
|
|
3455
3505
|
isError: true
|
|
@@ -3461,6 +3511,7 @@ var init_DispatchAgentTool = __esm({
|
|
|
3461
3511
|
|
|
3462
3512
|
... [TRUNCATED: Sub-agent response exceeded ${MAX_RESPONSE_CHARS} characters]`;
|
|
3463
3513
|
}
|
|
3514
|
+
this.signalStop({ description, ok: true, chars: finalResponse.length });
|
|
3464
3515
|
return {
|
|
3465
3516
|
content: `[dispatch_agent: ${description}]
|
|
3466
3517
|
|
|
@@ -3776,6 +3827,83 @@ Content-Type: ${contentType}
|
|
|
3776
3827
|
}
|
|
3777
3828
|
});
|
|
3778
3829
|
|
|
3830
|
+
// ../core/src/tools/WebSearchTool.ts
|
|
3831
|
+
var DEFAULT_MAX_RESULTS, MAX_RESULTS_CEILING, MAX_SNIPPET_CHARS, WebSearchTool;
|
|
3832
|
+
var init_WebSearchTool = __esm({
|
|
3833
|
+
"../core/src/tools/WebSearchTool.ts"() {
|
|
3834
|
+
"use strict";
|
|
3835
|
+
init_esm_shims();
|
|
3836
|
+
init_BaseTool();
|
|
3837
|
+
DEFAULT_MAX_RESULTS = 5;
|
|
3838
|
+
MAX_RESULTS_CEILING = 10;
|
|
3839
|
+
MAX_SNIPPET_CHARS = 500;
|
|
3840
|
+
WebSearchTool = class extends BaseTool {
|
|
3841
|
+
name = "web_search";
|
|
3842
|
+
description = "Search the live web for current information and return a ranked list of result titles, URLs, and snippets. Use this to find documentation, recent events, library versions, error explanations, or any topic where you need up-to-date sources you do not already have a URL for. Pair with web_fetch to read a specific result in full. Runs through the MSapling backend so no API keys are needed client-side.";
|
|
3843
|
+
parameters = {
|
|
3844
|
+
type: "object",
|
|
3845
|
+
required: ["query"],
|
|
3846
|
+
properties: {
|
|
3847
|
+
query: {
|
|
3848
|
+
type: "string",
|
|
3849
|
+
description: "The search query. Be specific for better results."
|
|
3850
|
+
},
|
|
3851
|
+
max_results: {
|
|
3852
|
+
type: "number",
|
|
3853
|
+
description: `Maximum number of results to return. Default: ${DEFAULT_MAX_RESULTS}. Max: ${MAX_RESULTS_CEILING}.`
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
client;
|
|
3858
|
+
constructor(options) {
|
|
3859
|
+
super();
|
|
3860
|
+
this.client = options.client;
|
|
3861
|
+
}
|
|
3862
|
+
async execute(args2, _projectRoot) {
|
|
3863
|
+
const rawQuery = args2?.query;
|
|
3864
|
+
if (!rawQuery || typeof rawQuery !== "string" || rawQuery.trim() === "") {
|
|
3865
|
+
return {
|
|
3866
|
+
content: 'Error: web_search requires a non-empty "query" argument.',
|
|
3867
|
+
isError: true
|
|
3868
|
+
};
|
|
3869
|
+
}
|
|
3870
|
+
const query = rawQuery.trim();
|
|
3871
|
+
let maxResults = DEFAULT_MAX_RESULTS;
|
|
3872
|
+
if (typeof args2?.max_results === "number" && args2.max_results > 0) {
|
|
3873
|
+
maxResults = Math.min(Math.floor(args2.max_results), MAX_RESULTS_CEILING);
|
|
3874
|
+
}
|
|
3875
|
+
let resp;
|
|
3876
|
+
try {
|
|
3877
|
+
resp = await this.client.webSearch(query, maxResults);
|
|
3878
|
+
} catch (e) {
|
|
3879
|
+
return {
|
|
3880
|
+
content: `Error: web_search failed \u2014 ${e?.message ?? String(e)}`,
|
|
3881
|
+
isError: true
|
|
3882
|
+
};
|
|
3883
|
+
}
|
|
3884
|
+
const results = Array.isArray(resp?.results) ? resp.results : [];
|
|
3885
|
+
if (results.length === 0) {
|
|
3886
|
+
return {
|
|
3887
|
+
content: `[web_search] No results for "${query}" (provider: ${resp?.provider ?? "none"}). Try rephrasing the query or use web_fetch if you already have a URL.`
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
const confidence = results[0]?.confidence ?? 0;
|
|
3891
|
+
const confidenceLabel = confidence >= 0.9 ? "high" : "low (scraped)";
|
|
3892
|
+
const header = `[web_search: ${results.length} result(s) for "${query}" \u2014 provider: ${resp.provider}, confidence: ${confidenceLabel}]
|
|
3893
|
+
|
|
3894
|
+
`;
|
|
3895
|
+
const body = results.map((r, i) => {
|
|
3896
|
+
const snippet = (r.snippet || "").slice(0, MAX_SNIPPET_CHARS);
|
|
3897
|
+
return `${i + 1}. ${r.title || "(untitled)"}
|
|
3898
|
+
${r.url || ""}
|
|
3899
|
+
${snippet}`;
|
|
3900
|
+
}).join("\n\n");
|
|
3901
|
+
return { content: header + body };
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3779
3907
|
// ../core/src/tools/BashTool.ts
|
|
3780
3908
|
import { resolve as resolve7, normalize as normalize6, relative as relative7, isAbsolute as isAbsolute7 } from "path";
|
|
3781
3909
|
import { spawn as spawn5 } from "child_process";
|
|
@@ -4953,6 +5081,65 @@ Backup: ${backedUpTo}`;
|
|
|
4953
5081
|
}
|
|
4954
5082
|
});
|
|
4955
5083
|
|
|
5084
|
+
// ../core/src/tools/PlanModeTools.ts
|
|
5085
|
+
var MAX_PLAN_CHARS, EnterPlanModeTool, ExitPlanModeTool;
|
|
5086
|
+
var init_PlanModeTools = __esm({
|
|
5087
|
+
"../core/src/tools/PlanModeTools.ts"() {
|
|
5088
|
+
"use strict";
|
|
5089
|
+
init_esm_shims();
|
|
5090
|
+
init_BaseTool();
|
|
5091
|
+
MAX_PLAN_CHARS = 2e4;
|
|
5092
|
+
EnterPlanModeTool = class extends BaseTool {
|
|
5093
|
+
name = "enter_plan_mode";
|
|
5094
|
+
description = "Switch into Plan Mode for a complex task that needs exploration before any changes. In Plan Mode you may ONLY use read-only tools (read_file, grep, glob, list_directory, web_fetch, web_search, etc.) \u2014 file writes and shell commands are blocked. Use this when the task is non-trivial and you should design an approach first. When your plan is ready, call exit_plan_mode to present it for the user's approval before executing.";
|
|
5095
|
+
parameters = {
|
|
5096
|
+
type: "object",
|
|
5097
|
+
properties: {}
|
|
5098
|
+
};
|
|
5099
|
+
async execute(_args, _projectRoot) {
|
|
5100
|
+
return {
|
|
5101
|
+
content: "Entered Plan Mode. This is a READ-ONLY exploration and design phase.\n1. Thoroughly explore the codebase to understand existing patterns.\n2. Identify similar features and architectural approaches.\n3. Consider multiple approaches and their trade-offs.\n4. Design a concrete implementation strategy.\n5. When ready, call exit_plan_mode with your finished plan for approval.\nDo NOT write or edit files or run commands until the plan is approved."
|
|
5102
|
+
};
|
|
5103
|
+
}
|
|
5104
|
+
};
|
|
5105
|
+
ExitPlanModeTool = class _ExitPlanModeTool extends BaseTool {
|
|
5106
|
+
name = "exit_plan_mode";
|
|
5107
|
+
description = "Present your finished implementation plan to the user for approval and exit Plan Mode. Only call this once you have a concrete plan. The plan is shown to the user; if they approve, Plan Mode is lifted and you may begin making changes. If they reject, you remain in Plan Mode and should revise the plan based on their feedback. Pass the full plan as Markdown in `plan`.";
|
|
5108
|
+
parameters = {
|
|
5109
|
+
type: "object",
|
|
5110
|
+
required: ["plan"],
|
|
5111
|
+
properties: {
|
|
5112
|
+
plan: {
|
|
5113
|
+
type: "string",
|
|
5114
|
+
description: "The full implementation plan, formatted as Markdown. Describe the steps you will take, the files you will change, and any trade-offs."
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
};
|
|
5118
|
+
/** Validate the plan argument. Returns an error string or null when valid. */
|
|
5119
|
+
static validatePlan(args2) {
|
|
5120
|
+
const plan = args2?.plan;
|
|
5121
|
+
if (!plan || typeof plan !== "string" || plan.trim() === "") {
|
|
5122
|
+
return 'Error: exit_plan_mode requires a non-empty "plan" argument (the implementation plan to present for approval).';
|
|
5123
|
+
}
|
|
5124
|
+
if (plan.length > MAX_PLAN_CHARS) {
|
|
5125
|
+
return `Error: plan is too long (${plan.length} chars; max ${MAX_PLAN_CHARS}). Summarize the plan.`;
|
|
5126
|
+
}
|
|
5127
|
+
return null;
|
|
5128
|
+
}
|
|
5129
|
+
async execute(args2, _projectRoot) {
|
|
5130
|
+
const err = _ExitPlanModeTool.validatePlan(args2);
|
|
5131
|
+
if (err) return { content: err, isError: true };
|
|
5132
|
+
return {
|
|
5133
|
+
content: `Plan recorded. (No interactive approval surface available \u2014 use /mode default or /mode acceptEdits to leave Plan Mode and begin execution.)
|
|
5134
|
+
|
|
5135
|
+
## Plan
|
|
5136
|
+
${String(args2.plan)}`
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
});
|
|
5142
|
+
|
|
4956
5143
|
// ../core/src/MDrive.ts
|
|
4957
5144
|
import { createHash as createHash2 } from "crypto";
|
|
4958
5145
|
var MDriveService;
|
|
@@ -5430,7 +5617,7 @@ async function runOne(entry, ctx) {
|
|
|
5430
5617
|
if (settled) return;
|
|
5431
5618
|
settled = true;
|
|
5432
5619
|
clearTimeout(killer);
|
|
5433
|
-
const blocked = !!entry.blocking && (exitCode === null || exitCode !== 0);
|
|
5620
|
+
const blocked = !NON_BLOCKING_EVENTS.has(ctx.event) && !!entry.blocking && (exitCode === null || exitCode !== 0);
|
|
5434
5621
|
resolve20({ command, exitCode, stdout, stderr, timedOut, blocked });
|
|
5435
5622
|
};
|
|
5436
5623
|
const killer = setTimeout(() => {
|
|
@@ -5467,11 +5654,20 @@ async function runOne(entry, ctx) {
|
|
|
5467
5654
|
}
|
|
5468
5655
|
});
|
|
5469
5656
|
}
|
|
5470
|
-
var DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
|
|
5657
|
+
var NON_BLOCKING_EVENTS, DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
|
|
5471
5658
|
var init_Hooks = __esm({
|
|
5472
5659
|
"../core/src/Hooks.ts"() {
|
|
5473
5660
|
"use strict";
|
|
5474
5661
|
init_esm_shims();
|
|
5662
|
+
NON_BLOCKING_EVENTS = /* @__PURE__ */ new Set([
|
|
5663
|
+
"post-tool-use",
|
|
5664
|
+
"session-start",
|
|
5665
|
+
"session-end",
|
|
5666
|
+
"stop",
|
|
5667
|
+
"subagent-stop",
|
|
5668
|
+
"pre-compact",
|
|
5669
|
+
"notification"
|
|
5670
|
+
]);
|
|
5475
5671
|
DEFAULT_TIMEOUT_MS3 = 5e3;
|
|
5476
5672
|
MAX_OUTPUT_BYTES3 = 32e3;
|
|
5477
5673
|
HookRunner = class {
|
|
@@ -5499,6 +5695,18 @@ var init_Hooks = __esm({
|
|
|
5499
5695
|
}
|
|
5500
5696
|
return outcomes;
|
|
5501
5697
|
}
|
|
5698
|
+
/**
|
|
5699
|
+
* P1-7a: fire an observation-only lifecycle event without awaiting or
|
|
5700
|
+
* blocking the caller. Errors are swallowed so a misbehaving hook can never
|
|
5701
|
+
* break session start/stop/teardown. Returns immediately; the hook chain
|
|
5702
|
+
* runs in the background. No-op if no hooks are registered for the event.
|
|
5703
|
+
*/
|
|
5704
|
+
fireForget(ctx) {
|
|
5705
|
+
const entries = this.hooks[ctx.event];
|
|
5706
|
+
if (!entries || entries.length === 0) return;
|
|
5707
|
+
void this.fire(ctx).catch(() => {
|
|
5708
|
+
});
|
|
5709
|
+
}
|
|
5502
5710
|
/** Convenience: did any outcome block? */
|
|
5503
5711
|
static anyBlocked(outcomes) {
|
|
5504
5712
|
return outcomes.find((o) => o.blocked) ?? null;
|
|
@@ -5834,12 +6042,14 @@ var init_ToolExecutor = __esm({
|
|
|
5834
6042
|
init_PatchFileTool();
|
|
5835
6043
|
init_DispatchAgentTool();
|
|
5836
6044
|
init_WebFetchTool();
|
|
6045
|
+
init_WebSearchTool();
|
|
5837
6046
|
init_BashTool();
|
|
5838
6047
|
init_NotebookReadTool();
|
|
5839
6048
|
init_NotebookEditTool();
|
|
5840
6049
|
init_MultiEditFileTool();
|
|
5841
6050
|
init_MoveFileTool();
|
|
5842
6051
|
init_DeleteFileTool();
|
|
6052
|
+
init_PlanModeTools();
|
|
5843
6053
|
init_MDrive();
|
|
5844
6054
|
init_Sandbox();
|
|
5845
6055
|
init_Voice();
|
|
@@ -5875,6 +6085,13 @@ var init_ToolExecutor = __esm({
|
|
|
5875
6085
|
sessionTrust = /* @__PURE__ */ new Set();
|
|
5876
6086
|
mcpRegistry = null;
|
|
5877
6087
|
hooks = null;
|
|
6088
|
+
/**
|
|
6089
|
+
* CLI-PARITY-P0-2: Notifier fired whenever a tool flips the permission mode
|
|
6090
|
+
* (enter_plan_mode / exit_plan_mode). Lets the CLI keep its React `mode`
|
|
6091
|
+
* state + footer in sync with a model-driven mode change. Optional — when
|
|
6092
|
+
* unset the mode still flips internally; only the UI display would lag.
|
|
6093
|
+
*/
|
|
6094
|
+
onModeChange = null;
|
|
5878
6095
|
constructor(client, projectRoot) {
|
|
5879
6096
|
this.sandbox = new Sandbox(projectRoot);
|
|
5880
6097
|
this.mdrive = new MDriveService(client);
|
|
@@ -5894,15 +6111,28 @@ var init_ToolExecutor = __esm({
|
|
|
5894
6111
|
this.registerTool(new PatchFileTool());
|
|
5895
6112
|
this.registerTool(new DispatchAgentTool({
|
|
5896
6113
|
client,
|
|
5897
|
-
projectRoot
|
|
6114
|
+
projectRoot,
|
|
6115
|
+
// P1-7a (CLI-HOOKS-LIFECYCLE-01): fire the subagent-stop lifecycle hook
|
|
6116
|
+
// when a dispatched sub-agent finishes. Observation-only / non-blocking.
|
|
6117
|
+
onSubagentStop: (info) => {
|
|
6118
|
+
this.hooks?.fireForget({
|
|
6119
|
+
event: "subagent-stop",
|
|
6120
|
+
tool: info.description,
|
|
6121
|
+
payload: info,
|
|
6122
|
+
cwd: projectRoot
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
5898
6125
|
}));
|
|
5899
6126
|
this.registerTool(new WebFetchTool());
|
|
6127
|
+
this.registerTool(new WebSearchTool({ client }));
|
|
5900
6128
|
this.registerTool(new BashTool());
|
|
5901
6129
|
this.registerTool(new NotebookReadTool());
|
|
5902
6130
|
this.registerTool(new NotebookEditTool());
|
|
5903
6131
|
this.registerTool(new MultiEditFileTool());
|
|
5904
6132
|
this.registerTool(new MoveFileTool());
|
|
5905
6133
|
this.registerTool(new DeleteFileTool());
|
|
6134
|
+
this.registerTool(new EnterPlanModeTool());
|
|
6135
|
+
this.registerTool(new ExitPlanModeTool());
|
|
5906
6136
|
}
|
|
5907
6137
|
setToolsEnabled(enabled) {
|
|
5908
6138
|
this.toolsEnabled = enabled;
|
|
@@ -5913,6 +6143,24 @@ var init_ToolExecutor = __esm({
|
|
|
5913
6143
|
getMode() {
|
|
5914
6144
|
return this.mode;
|
|
5915
6145
|
}
|
|
6146
|
+
/**
|
|
6147
|
+
* CLI-PARITY-P0-2: Register a callback invoked when a tool changes the
|
|
6148
|
+
* permission mode (enter_plan_mode / exit_plan_mode), so the CLI can mirror
|
|
6149
|
+
* the new mode into its React state. Does not affect the internal mode.
|
|
6150
|
+
*/
|
|
6151
|
+
setOnModeChange(cb) {
|
|
6152
|
+
this.onModeChange = cb;
|
|
6153
|
+
}
|
|
6154
|
+
/** Internal: flip the mode AND notify any registered listener. */
|
|
6155
|
+
applyModeChange(mode) {
|
|
6156
|
+
this.mode = mode;
|
|
6157
|
+
if (this.onModeChange) {
|
|
6158
|
+
try {
|
|
6159
|
+
this.onModeChange(mode);
|
|
6160
|
+
} catch {
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
}
|
|
5916
6164
|
setApprovalCallback(cb) {
|
|
5917
6165
|
this.approvalCallback = cb;
|
|
5918
6166
|
}
|
|
@@ -5957,6 +6205,61 @@ var init_ToolExecutor = __esm({
|
|
|
5957
6205
|
registerTool(tool) {
|
|
5958
6206
|
this.tools.set(tool.name, tool);
|
|
5959
6207
|
}
|
|
6208
|
+
/**
|
|
6209
|
+
* CLI-PARITY-P0-2: enter_plan_mode handler. Flips the executor into read-only
|
|
6210
|
+
* `plan` mode (idempotent) and returns the research instructions. No approval:
|
|
6211
|
+
* entering plan mode only tightens permissions.
|
|
6212
|
+
*/
|
|
6213
|
+
async handleEnterPlanMode() {
|
|
6214
|
+
if (this.mode !== "plan") {
|
|
6215
|
+
this.applyModeChange("plan");
|
|
6216
|
+
}
|
|
6217
|
+
const tool = this.tools.get("enter_plan_mode");
|
|
6218
|
+
return tool.execute({}, "");
|
|
6219
|
+
}
|
|
6220
|
+
/**
|
|
6221
|
+
* CLI-PARITY-P0-2: exit_plan_mode handler. Validates the drafted plan,
|
|
6222
|
+
* presents it through the existing approval UI (approvalCallback), and on
|
|
6223
|
+
* approve flips `plan` → default/acceptEdits so execution can proceed. On deny
|
|
6224
|
+
* the executor stays in plan mode and the model is told to revise.
|
|
6225
|
+
*/
|
|
6226
|
+
async handleExitPlanMode(args2) {
|
|
6227
|
+
const err = ExitPlanModeTool.validatePlan(args2);
|
|
6228
|
+
if (err) return { content: err, isError: true };
|
|
6229
|
+
if (this.mode !== "plan") {
|
|
6230
|
+
return {
|
|
6231
|
+
content: "You are not in Plan Mode, so there is nothing to exit. Continue with implementation."
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
const plan = String(args2.plan);
|
|
6235
|
+
if (!this.approvalCallback) {
|
|
6236
|
+
this.applyModeChange("default");
|
|
6237
|
+
return {
|
|
6238
|
+
content: `No interactive approval surface available \u2014 Plan Mode lifted (now in default mode). Proceed with the plan.
|
|
6239
|
+
|
|
6240
|
+
## Approved Plan
|
|
6241
|
+
${plan}`
|
|
6242
|
+
};
|
|
6243
|
+
}
|
|
6244
|
+
const decision = await this.approvalCallback({
|
|
6245
|
+
tool: "exit_plan_mode",
|
|
6246
|
+
command: plan,
|
|
6247
|
+
reason: "The agent has finished planning and wants to start executing this plan. Approve to leave Plan Mode."
|
|
6248
|
+
});
|
|
6249
|
+
if (decision === "no") {
|
|
6250
|
+
return {
|
|
6251
|
+
content: "User rejected the plan. You remain in Plan Mode. Ask the user for specific feedback and revise the plan, then call exit_plan_mode again."
|
|
6252
|
+
};
|
|
6253
|
+
}
|
|
6254
|
+
const targetMode = decision === "always" ? "acceptEdits" : "default";
|
|
6255
|
+
this.applyModeChange(targetMode);
|
|
6256
|
+
return {
|
|
6257
|
+
content: `User approved the plan. Plan Mode lifted (now in ${targetMode} mode). You can now start executing. Begin with the first step.
|
|
6258
|
+
|
|
6259
|
+
## Approved Plan
|
|
6260
|
+
${plan}`
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
5960
6263
|
async execute(toolName, args2, projectRoot) {
|
|
5961
6264
|
const tool = this.tools.get(toolName);
|
|
5962
6265
|
if (!tool) {
|
|
@@ -5972,6 +6275,12 @@ var init_ToolExecutor = __esm({
|
|
|
5972
6275
|
}
|
|
5973
6276
|
return { content: `Error: unknown tool: ${toolName}`, isError: true };
|
|
5974
6277
|
}
|
|
6278
|
+
if (toolName === "enter_plan_mode") {
|
|
6279
|
+
return this.handleEnterPlanMode();
|
|
6280
|
+
}
|
|
6281
|
+
if (toolName === "exit_plan_mode") {
|
|
6282
|
+
return this.handleExitPlanMode(args2);
|
|
6283
|
+
}
|
|
5975
6284
|
if (this.mode === "plan") {
|
|
5976
6285
|
if (APPROVAL_GATED.has(toolName)) {
|
|
5977
6286
|
return {
|
|
@@ -6396,6 +6705,14 @@ var init_Agent = __esm({
|
|
|
6396
6705
|
getMode() {
|
|
6397
6706
|
return this.executor.getMode();
|
|
6398
6707
|
}
|
|
6708
|
+
/**
|
|
6709
|
+
* CLI-PARITY-P0-2: Register a listener for model-driven mode changes
|
|
6710
|
+
* (enter_plan_mode / exit_plan_mode) so the CLI can mirror the new mode into
|
|
6711
|
+
* its own state + footer.
|
|
6712
|
+
*/
|
|
6713
|
+
setOnModeChange(cb) {
|
|
6714
|
+
this.executor.setOnModeChange(cb);
|
|
6715
|
+
}
|
|
6399
6716
|
setApprovalCallback(cb) {
|
|
6400
6717
|
this.executor.setApprovalCallback(cb);
|
|
6401
6718
|
}
|
|
@@ -6406,6 +6723,19 @@ var init_Agent = __esm({
|
|
|
6406
6723
|
this.executor.setHookRunner(runner);
|
|
6407
6724
|
this.hooks = runner;
|
|
6408
6725
|
}
|
|
6726
|
+
/** Expose the active HookRunner (e.g. for App-level session-start/end). */
|
|
6727
|
+
getHookRunner() {
|
|
6728
|
+
return this.hooks;
|
|
6729
|
+
}
|
|
6730
|
+
/**
|
|
6731
|
+
* P1-7a: fire an observation-only lifecycle hook event without blocking.
|
|
6732
|
+
* No-op when no HookRunner is wired. `name` is the descriptive string used
|
|
6733
|
+
* for matcher evaluation (e.g. the subagent description); `payload` is the
|
|
6734
|
+
* JSON event body delivered on the hook's stdin.
|
|
6735
|
+
*/
|
|
6736
|
+
fireLifecycleHook(event, payload, name = "") {
|
|
6737
|
+
this.hooks?.fireForget({ event, tool: name, payload, cwd: this.projectRoot });
|
|
6738
|
+
}
|
|
6409
6739
|
/**
|
|
6410
6740
|
* CLI-PARITY-37: Wire a pre-loaded TrustStore so "always" approval decisions
|
|
6411
6741
|
* are persisted across CLI restarts. Must be called after `store.load()`.
|
|
@@ -6465,6 +6795,11 @@ var init_Agent = __esm({
|
|
|
6465
6795
|
`
|
|
6466
6796
|
[Agent: context budget reached \u2014 auto-compacting conversation (${this.contextBudget.summary()})]`
|
|
6467
6797
|
);
|
|
6798
|
+
this.fireLifecycleHook(
|
|
6799
|
+
"pre-compact",
|
|
6800
|
+
{ chat_id: chatId, reason: "auto", budget: this.contextBudget.summary() },
|
|
6801
|
+
"auto"
|
|
6802
|
+
);
|
|
6468
6803
|
rounds++;
|
|
6469
6804
|
const compactionStream = this.client.streamChat({
|
|
6470
6805
|
chat_id: chatId,
|
|
@@ -6547,6 +6882,7 @@ ${next}`;
|
|
|
6547
6882
|
if (rounds >= MAX_WORKER_TURN_DEPTH) {
|
|
6548
6883
|
throw new Error("runWorkerTurn exceeded MAX_DEPTH");
|
|
6549
6884
|
}
|
|
6885
|
+
this.fireLifecycleHook("stop", { chat_id: chatId, rounds }, "");
|
|
6550
6886
|
return streamUsage;
|
|
6551
6887
|
}
|
|
6552
6888
|
async run(prompt4, model) {
|
|
@@ -8040,11 +8376,14 @@ __export(src_exports2, {
|
|
|
8040
8376
|
DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
|
|
8041
8377
|
DeleteFileTool: () => DeleteFileTool,
|
|
8042
8378
|
DispatchAgentTool: () => DispatchAgentTool,
|
|
8379
|
+
EnterPlanModeTool: () => EnterPlanModeTool,
|
|
8380
|
+
ExitPlanModeTool: () => ExitPlanModeTool,
|
|
8043
8381
|
GlobFilesTool: () => GlobFilesTool,
|
|
8044
8382
|
GrepSearchTool: () => GrepSearchTool,
|
|
8045
8383
|
HookRunner: () => HookRunner,
|
|
8046
8384
|
KeychainUnavailableError: () => KeychainUnavailableError,
|
|
8047
8385
|
ListDirectoryTool: () => ListDirectoryTool,
|
|
8386
|
+
MAX_PLAN_CHARS: () => MAX_PLAN_CHARS,
|
|
8048
8387
|
MCPClient: () => MCPClient,
|
|
8049
8388
|
MCPClientError: () => MCPClientError,
|
|
8050
8389
|
MCPRegistry: () => MCPRegistry,
|
|
@@ -8061,6 +8400,7 @@ __export(src_exports2, {
|
|
|
8061
8400
|
ToolExecutor: () => ToolExecutor,
|
|
8062
8401
|
TrustStore: () => TrustStore,
|
|
8063
8402
|
WebFetchTool: () => WebFetchTool,
|
|
8403
|
+
WebSearchTool: () => WebSearchTool,
|
|
8064
8404
|
WriteFileTool: () => WriteFileTool,
|
|
8065
8405
|
buildCompactionPrompt: () => buildCompactionPrompt,
|
|
8066
8406
|
buildHwContext: () => buildHwContext,
|
|
@@ -8099,6 +8439,8 @@ var init_src3 = __esm({
|
|
|
8099
8439
|
init_TodoTools();
|
|
8100
8440
|
init_DispatchAgentTool();
|
|
8101
8441
|
init_WebFetchTool();
|
|
8442
|
+
init_WebSearchTool();
|
|
8443
|
+
init_PlanModeTools();
|
|
8102
8444
|
init_BashTool();
|
|
8103
8445
|
init_NotebookReadTool();
|
|
8104
8446
|
init_NotebookEditTool();
|
|
@@ -9037,6 +9379,104 @@ var init_chat = __esm({
|
|
|
9037
9379
|
}
|
|
9038
9380
|
});
|
|
9039
9381
|
|
|
9382
|
+
// src/commands/resume.ts
|
|
9383
|
+
var resume_exports = {};
|
|
9384
|
+
__export(resume_exports, {
|
|
9385
|
+
rehydrateChat: () => rehydrateChat,
|
|
9386
|
+
resolveRecentChat: () => resolveRecentChat,
|
|
9387
|
+
resumeCommand: () => resumeCommand
|
|
9388
|
+
});
|
|
9389
|
+
function labelFor2(i) {
|
|
9390
|
+
return i < 9 ? String(i + 1) : LETTERS2[i - 9] ?? `#${i + 1}`;
|
|
9391
|
+
}
|
|
9392
|
+
function resolveRecentChat(arg, chats) {
|
|
9393
|
+
const trimmed = arg.trim();
|
|
9394
|
+
if (!trimmed) return { kind: "error", message: "empty argument" };
|
|
9395
|
+
const idHit = chats.find((c) => c.id === trimmed);
|
|
9396
|
+
if (idHit) return { kind: "ok", chat: idHit };
|
|
9397
|
+
if (/^\d+$/.test(trimmed)) {
|
|
9398
|
+
const n = parseInt(trimmed, 10);
|
|
9399
|
+
if (n >= 1 && n <= chats.length) return { kind: "ok", chat: chats[n - 1] };
|
|
9400
|
+
return { kind: "error", message: `no chat at index ${n} (have ${chats.length})` };
|
|
9401
|
+
}
|
|
9402
|
+
if (/^[a-zA-Z]$/.test(trimmed)) {
|
|
9403
|
+
const idx = 9 + LETTERS2.indexOf(trimmed.toLowerCase());
|
|
9404
|
+
if (idx >= 9 && idx < chats.length) return { kind: "ok", chat: chats[idx] };
|
|
9405
|
+
}
|
|
9406
|
+
return {
|
|
9407
|
+
kind: "error",
|
|
9408
|
+
message: `no recent chat matches '${trimmed}'. Run /resume (no args) for the list.`
|
|
9409
|
+
};
|
|
9410
|
+
}
|
|
9411
|
+
async function rehydrateChat(chat, context) {
|
|
9412
|
+
context.clearHistory();
|
|
9413
|
+
context.setActiveChatId(chat.id);
|
|
9414
|
+
let messages = [];
|
|
9415
|
+
try {
|
|
9416
|
+
messages = await context.client.getHistory(chat.id);
|
|
9417
|
+
} catch (e) {
|
|
9418
|
+
context.addMessage("error", `Resumed ${chat.id} but failed to load history: ${e?.message ?? e}`);
|
|
9419
|
+
}
|
|
9420
|
+
const label = chat.title ?? chat.id;
|
|
9421
|
+
const where = chat.project ? ` in '${chat.project}'` : "";
|
|
9422
|
+
context.addMessage("system", `Resumed chat "${label}"${where} (${messages.length} message(s) replayed).`);
|
|
9423
|
+
for (const m of messages) {
|
|
9424
|
+
const role = m.role === "user" ? "user" : m.role === "assistant" ? "assistant" : "system";
|
|
9425
|
+
context.addMessage(role, m.content);
|
|
9426
|
+
}
|
|
9427
|
+
try {
|
|
9428
|
+
await context.refreshOverview();
|
|
9429
|
+
} catch {
|
|
9430
|
+
}
|
|
9431
|
+
return messages.length;
|
|
9432
|
+
}
|
|
9433
|
+
async function listAndResume(args2, context) {
|
|
9434
|
+
let chats;
|
|
9435
|
+
try {
|
|
9436
|
+
chats = await context.client.listRecentChats(35);
|
|
9437
|
+
} catch (e) {
|
|
9438
|
+
context.addMessage("error", `Failed to list recent chats: ${e?.message ?? e}`);
|
|
9439
|
+
return;
|
|
9440
|
+
}
|
|
9441
|
+
if (chats.length === 0) {
|
|
9442
|
+
context.addMessage("system", "No prior chats found. Send a message to start one.");
|
|
9443
|
+
return;
|
|
9444
|
+
}
|
|
9445
|
+
const arg = args2.join(" ").trim();
|
|
9446
|
+
if (!arg) {
|
|
9447
|
+
context.addMessage("system", "Recent chats \u2014 /resume <number|letter|id> to reopen:");
|
|
9448
|
+
chats.forEach((c, i) => {
|
|
9449
|
+
const star = c.id === context.activeChatId ? " \u2605" : "";
|
|
9450
|
+
const project = c.project ? ` (${c.project})` : "";
|
|
9451
|
+
const model = c.model ? ` [${c.model}]` : "";
|
|
9452
|
+
context.addMessage("system", ` ${labelFor2(i)}. ${c.title ?? c.id}${project}${model}${star}`);
|
|
9453
|
+
});
|
|
9454
|
+
return;
|
|
9455
|
+
}
|
|
9456
|
+
const resolved = resolveRecentChat(arg, chats);
|
|
9457
|
+
if (resolved.kind === "error") {
|
|
9458
|
+
context.addMessage("error", resolved.message);
|
|
9459
|
+
return;
|
|
9460
|
+
}
|
|
9461
|
+
await rehydrateChat(resolved.chat, context);
|
|
9462
|
+
}
|
|
9463
|
+
var LETTERS2, resumeCommand;
|
|
9464
|
+
var init_resume = __esm({
|
|
9465
|
+
"src/commands/resume.ts"() {
|
|
9466
|
+
"use strict";
|
|
9467
|
+
init_esm_shims();
|
|
9468
|
+
LETTERS2 = "abcdefghijklmnopqrstuvwxyz";
|
|
9469
|
+
resumeCommand = {
|
|
9470
|
+
name: "resume",
|
|
9471
|
+
aliases: ["continue"],
|
|
9472
|
+
args: "[number|letter|id]",
|
|
9473
|
+
description: "List recent chats and reopen one \u2014 replays its history into the REPL",
|
|
9474
|
+
category: "chat",
|
|
9475
|
+
handler: async (args2, context) => listAndResume(args2, context)
|
|
9476
|
+
};
|
|
9477
|
+
}
|
|
9478
|
+
});
|
|
9479
|
+
|
|
9040
9480
|
// src/commands/broadcast.ts
|
|
9041
9481
|
var broadcastCommand;
|
|
9042
9482
|
var init_broadcast = __esm({
|
|
@@ -9913,8 +10353,8 @@ var init_memory = __esm({
|
|
|
9913
10353
|
});
|
|
9914
10354
|
|
|
9915
10355
|
// src/commands/project.ts
|
|
9916
|
-
function
|
|
9917
|
-
return i < 9 ? String(i + 1) :
|
|
10356
|
+
function labelFor3(i) {
|
|
10357
|
+
return i < 9 ? String(i + 1) : LETTERS3[i - 9] ?? `#${i + 1}`;
|
|
9918
10358
|
}
|
|
9919
10359
|
function resolveProject(arg, list2) {
|
|
9920
10360
|
const trimmed = arg.trim();
|
|
@@ -9927,7 +10367,7 @@ function resolveProject(arg, list2) {
|
|
|
9927
10367
|
return { kind: "error", message: `no project at index ${n} (have ${list2.length})` };
|
|
9928
10368
|
}
|
|
9929
10369
|
if (/^[a-zA-Z]$/.test(trimmed)) {
|
|
9930
|
-
const idx = 9 +
|
|
10370
|
+
const idx = 9 + LETTERS3.indexOf(trimmed.toLowerCase());
|
|
9931
10371
|
if (idx >= 9 && idx < list2.length) return { kind: "ok", id: list2[idx].id };
|
|
9932
10372
|
}
|
|
9933
10373
|
const lower = trimmed.toLowerCase();
|
|
@@ -9941,12 +10381,12 @@ function resolveProject(arg, list2) {
|
|
|
9941
10381
|
}
|
|
9942
10382
|
return { kind: "error", message: `no project matches '${trimmed}'. Run /project (no args) for the list.` };
|
|
9943
10383
|
}
|
|
9944
|
-
var
|
|
10384
|
+
var LETTERS3, projectCommand;
|
|
9945
10385
|
var init_project = __esm({
|
|
9946
10386
|
"src/commands/project.ts"() {
|
|
9947
10387
|
"use strict";
|
|
9948
10388
|
init_esm_shims();
|
|
9949
|
-
|
|
10389
|
+
LETTERS3 = "abcdefghijklmnopqrstuvwxyz";
|
|
9950
10390
|
projectCommand = {
|
|
9951
10391
|
name: "project",
|
|
9952
10392
|
args: "[number|letter|name|id|config]",
|
|
@@ -10005,7 +10445,7 @@ var init_project = __esm({
|
|
|
10005
10445
|
}
|
|
10006
10446
|
context.addMessage("system", "Available Projects:");
|
|
10007
10447
|
projects.forEach((p, i) => {
|
|
10008
|
-
const label =
|
|
10448
|
+
const label = labelFor3(i);
|
|
10009
10449
|
const star = p.id === currentId ? " \u2605" : "";
|
|
10010
10450
|
const usage = p.chat_count != null && p.chat_limit != null ? ` (${p.chat_count}/${p.chat_limit} chats)` : "";
|
|
10011
10451
|
context.addMessage("system", ` ${label}. ${p.name ?? p.id}${usage}${star}`);
|
|
@@ -10616,7 +11056,7 @@ var init_hooks = __esm({
|
|
|
10616
11056
|
init_src3();
|
|
10617
11057
|
hooksCommand = {
|
|
10618
11058
|
name: "hooks",
|
|
10619
|
-
description: "List configured lifecycle hooks (pre-tool-use,
|
|
11059
|
+
description: "List configured lifecycle hooks (pre/post-tool-use, user-prompt-submit, session-start/end, stop, subagent-stop, pre-compact, notification)",
|
|
10620
11060
|
category: "debug",
|
|
10621
11061
|
handler: async (args2, context) => {
|
|
10622
11062
|
const sub = args2[0]?.toLowerCase();
|
|
@@ -10920,7 +11360,7 @@ var init_version = __esm({
|
|
|
10920
11360
|
description: "Show version information for CLI and core packages",
|
|
10921
11361
|
category: "debug",
|
|
10922
11362
|
handler: async (_args, context) => {
|
|
10923
|
-
const cliVersion = true ? "2.3.6-beta.
|
|
11363
|
+
const cliVersion = true ? "2.3.6-beta.46" : "(dev)";
|
|
10924
11364
|
const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
|
|
10925
11365
|
const runtime = process.version;
|
|
10926
11366
|
context.addMessage("system", "MSapling Version Info");
|
|
@@ -10929,7 +11369,7 @@ var init_version = __esm({
|
|
|
10929
11369
|
context.addMessage("system", row2("Core (@msapling/core)", coreVersion));
|
|
10930
11370
|
context.addMessage("system", row2("Runtime (Node/Bun)", runtime));
|
|
10931
11371
|
try {
|
|
10932
|
-
const ts = "2026-06-20T07:
|
|
11372
|
+
const ts = "2026-06-20T07:22:28.613Z";
|
|
10933
11373
|
if (ts && ts !== "__BUILD_TIMESTAMP__") {
|
|
10934
11374
|
context.addMessage("system", row2("Build Timestamp", ts));
|
|
10935
11375
|
}
|
|
@@ -12689,6 +13129,7 @@ var init_commands = __esm({
|
|
|
12689
13129
|
init_exit();
|
|
12690
13130
|
init_help();
|
|
12691
13131
|
init_chat();
|
|
13132
|
+
init_resume();
|
|
12692
13133
|
init_broadcast();
|
|
12693
13134
|
init_ollama();
|
|
12694
13135
|
init_keys();
|
|
@@ -12752,6 +13193,7 @@ var init_commands = __esm({
|
|
|
12752
13193
|
helpCommand,
|
|
12753
13194
|
chatCommand,
|
|
12754
13195
|
chatsCommand,
|
|
13196
|
+
resumeCommand,
|
|
12755
13197
|
broadcastCommand,
|
|
12756
13198
|
ollamaCommand,
|
|
12757
13199
|
keysCommand,
|
|
@@ -16462,7 +16904,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
16462
16904
|
var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
|
|
16463
16905
|
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
16464
16906
|
"\u25CF MSapling CLI v",
|
|
16465
|
-
"2.3.6-beta.
|
|
16907
|
+
"2.3.6-beta.46"
|
|
16466
16908
|
] }),
|
|
16467
16909
|
/* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
|
|
16468
16910
|
] });
|
|
@@ -17263,7 +17705,7 @@ function useTerminalResize() {
|
|
|
17263
17705
|
|
|
17264
17706
|
// src/App.tsx
|
|
17265
17707
|
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
17266
|
-
var App = ({ compact: compact2 = false }) => {
|
|
17708
|
+
var App = ({ compact: compact2 = false, continueSession: continueSession2 = false }) => {
|
|
17267
17709
|
const [user, setUser] = useState4(null);
|
|
17268
17710
|
const [input, setInput] = useState4("");
|
|
17269
17711
|
const [history, setHistory] = useState4([]);
|
|
@@ -17285,18 +17727,32 @@ var App = ({ compact: compact2 = false }) => {
|
|
|
17285
17727
|
const { columns: termResizeCols, rows: termResizeRows } = useTerminalResize();
|
|
17286
17728
|
const storage = useRef(new StorageManager()).current;
|
|
17287
17729
|
const client = useRef(new MSaplingClient()).current;
|
|
17730
|
+
const agentRef = useRef(null);
|
|
17288
17731
|
const requestApproval = useCallback2((request) => {
|
|
17732
|
+
agentRef.current?.fireLifecycleHook(
|
|
17733
|
+
"notification",
|
|
17734
|
+
{ kind: "approval-request", tool: request.tool, command: request.command, reason: request.reason },
|
|
17735
|
+
request.tool
|
|
17736
|
+
);
|
|
17289
17737
|
return new Promise((resolve20) => {
|
|
17290
17738
|
setPendingApproval({ request, resolve: resolve20 });
|
|
17291
17739
|
});
|
|
17292
17740
|
}, []);
|
|
17293
17741
|
const agent = useRef(new Agent(client, process.cwd(), requestApproval)).current;
|
|
17742
|
+
agentRef.current = agent;
|
|
17294
17743
|
const trustStore = useRef(new TrustStore()).current;
|
|
17295
17744
|
const lastActivityRef = useRef(Date.now());
|
|
17296
17745
|
const pollingIntervalRef = useRef(null);
|
|
17297
17746
|
useEffect4(() => {
|
|
17298
17747
|
agent.setApprovalCallback(requestApproval);
|
|
17299
17748
|
}, [agent, requestApproval]);
|
|
17749
|
+
useEffect4(() => {
|
|
17750
|
+
agent.setOnModeChange((m) => {
|
|
17751
|
+
setModeState(m);
|
|
17752
|
+
addMessage("system", `Mode changed to: ${m} (via plan-mode tool)`);
|
|
17753
|
+
});
|
|
17754
|
+
return () => agent.setOnModeChange(null);
|
|
17755
|
+
}, [agent]);
|
|
17300
17756
|
const resolveApproval = useCallback2((decision) => {
|
|
17301
17757
|
setPendingApproval((current) => {
|
|
17302
17758
|
current?.resolve(decision);
|
|
@@ -17358,21 +17814,53 @@ var App = ({ compact: compact2 = false }) => {
|
|
|
17358
17814
|
}
|
|
17359
17815
|
}, [client, activeChatId, activeProjectId, setProjectId, handle401]);
|
|
17360
17816
|
useEffect4(() => {
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17817
|
+
(async () => {
|
|
17818
|
+
await initSession({
|
|
17819
|
+
agent,
|
|
17820
|
+
client,
|
|
17821
|
+
storage,
|
|
17822
|
+
trustStore,
|
|
17823
|
+
setActiveModel,
|
|
17824
|
+
setMode,
|
|
17825
|
+
setStatus,
|
|
17826
|
+
addMessage,
|
|
17827
|
+
refreshOverview,
|
|
17828
|
+
setShellEscapeEnabled,
|
|
17829
|
+
// CLI-PORT-STATE-04: restore persisted project/chat on cold start.
|
|
17830
|
+
setProjectId,
|
|
17831
|
+
setActiveChatId
|
|
17832
|
+
});
|
|
17833
|
+
if (continueSession2) {
|
|
17834
|
+
try {
|
|
17835
|
+
const { rehydrateChat: rehydrateChat2 } = await Promise.resolve().then(() => (init_resume(), resume_exports));
|
|
17836
|
+
const recent = await client.listRecentChats(1);
|
|
17837
|
+
if (recent.length > 0) {
|
|
17838
|
+
await rehydrateChat2(recent[0], {
|
|
17839
|
+
client,
|
|
17840
|
+
setActiveChatId,
|
|
17841
|
+
clearHistory,
|
|
17842
|
+
addMessage,
|
|
17843
|
+
refreshOverview
|
|
17844
|
+
});
|
|
17845
|
+
} else {
|
|
17846
|
+
addMessage("system", "--continue: no prior chats to resume.");
|
|
17847
|
+
}
|
|
17848
|
+
} catch (e) {
|
|
17849
|
+
addMessage("system", `--continue: could not resume most-recent chat: ${e?.message ?? e}`);
|
|
17850
|
+
}
|
|
17851
|
+
}
|
|
17852
|
+
agent.fireLifecycleHook("session-start", {
|
|
17853
|
+
cwd: process.cwd(),
|
|
17854
|
+
compact: compact2,
|
|
17855
|
+
continued: continueSession2,
|
|
17856
|
+
chat_id: activeChatId
|
|
17857
|
+
});
|
|
17858
|
+
})();
|
|
17859
|
+
}, []);
|
|
17860
|
+
useEffect4(() => {
|
|
17861
|
+
return () => {
|
|
17862
|
+
agent.fireLifecycleHook("session-end", { cwd: process.cwd() });
|
|
17863
|
+
};
|
|
17376
17864
|
}, []);
|
|
17377
17865
|
useEffect4(() => {
|
|
17378
17866
|
if (!user) return;
|
|
@@ -17531,6 +18019,7 @@ function handleCliArgs(args2) {
|
|
|
17531
18019
|
console.log("msapling \u2014 MSapling CLI (React/Ink)");
|
|
17532
18020
|
console.log("Usage: msapling start interactive REPL");
|
|
17533
18021
|
console.log(" msapling --compact start REPL in compact mode (no footer, thin separators)");
|
|
18022
|
+
console.log(" msapling --continue resume your most-recent chat (alias: -c); replays its history");
|
|
17534
18023
|
console.log(' msapling --exec "<cmd>" run one slash command non-interactively and exit');
|
|
17535
18024
|
console.log(" msapling mcp serve run as MCP stdio server (Claude Code / Cursor / Windsurf integration)");
|
|
17536
18025
|
console.log(" msapling doctor run diagnostic health checks");
|
|
@@ -17645,9 +18134,10 @@ if (!process.env.NODE_ENV?.includes("test")) {
|
|
|
17645
18134
|
}
|
|
17646
18135
|
var args = process.argv.slice(2);
|
|
17647
18136
|
var compact = args.includes("--compact");
|
|
18137
|
+
var continueSession = args.includes("--continue") || args.includes("-c");
|
|
17648
18138
|
var shouldRenderRepl = handleCliArgs(args);
|
|
17649
18139
|
if (shouldRenderRepl && !process.env.NODE_ENV?.includes("test")) {
|
|
17650
|
-
render(/* @__PURE__ */ jsx9(App, { compact }));
|
|
18140
|
+
render(/* @__PURE__ */ jsx9(App, { compact, continueSession }));
|
|
17651
18141
|
}
|
|
17652
18142
|
export {
|
|
17653
18143
|
App,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtreeai/msapling-cli",
|
|
3
|
-
"version": "2.3.6-beta.
|
|
3
|
+
"version": "2.3.6-beta.46",
|
|
4
4
|
"description": "MSapling CLI — React/Ink terminal client for the MSapling backend (chat, projects, MDrive, agent tools). Proprietary; redistribution prohibited.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "MSapling Team",
|
|
@@ -51,7 +51,9 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"ink": "^4.4.1",
|
|
54
|
+
"proper-lockfile": "^4.1.2",
|
|
54
55
|
"react": "^18.3.1",
|
|
56
|
+
"shell-quote": "^1.8.1",
|
|
55
57
|
"yaml": "^2.8.3"
|
|
56
58
|
},
|
|
57
59
|
"optionalDependencies": {
|