@openma/deepseek-harness-acp 0.4.3 → 0.4.5

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.
Files changed (3) hide show
  1. package/dist/bridge.js +191 -69
  2. package/dist/index.js +308 -71
  3. package/package.json +1 -1
package/dist/bridge.js CHANGED
@@ -863,6 +863,87 @@ async function apply(ctx, config = {}) {
863
863
  if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
864
864
  return record;
865
865
  };
866
+ const requireOrRestoreSession = async (sessionId) => {
867
+ const record = sessions.get(sessionId);
868
+ if (record !== void 0) return record;
869
+ if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
870
+ logWarn(`restoring session ${sessionId} from the persisted log`);
871
+ try {
872
+ return await restoreSession(sessionId, { replay: false });
873
+ } catch (error) {
874
+ throw invalidParams(`unknown session: ${sessionId} (${errorChain2(error)})`);
875
+ }
876
+ };
877
+ const restoreSession = async (sessionId, options) => {
878
+ const persistence = requirePersistence();
879
+ const existing = sessions.get(sessionId);
880
+ if (existing !== void 0) {
881
+ sessions.delete(sessionId);
882
+ existing.agent.cancel({ kind: "user" });
883
+ settlePrompt(existing, "cancelled");
884
+ await existing.dispose().catch((error) => {
885
+ logWarn(`dispose before reload failed: ${String(error)}`);
886
+ });
887
+ }
888
+ let events;
889
+ let storedHeader;
890
+ try {
891
+ const inspected = await persistence.inspect(SessionId2(sessionId));
892
+ events = inspected.events;
893
+ storedHeader = inspected.header;
894
+ } catch (error) {
895
+ throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
896
+ }
897
+ const replay = buildReplay(events);
898
+ if (options.replay) {
899
+ for (const update of replay.updates) notify(sessionId, update);
900
+ if (replay.title !== void 0) {
901
+ notify(sessionId, {
902
+ sessionUpdate: "session_info_update",
903
+ title: replay.title
904
+ });
905
+ }
906
+ }
907
+ const presets = presetsService();
908
+ const storedPreset = presetFromLog(storedHeader, events);
909
+ const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
910
+ const handle = await agents.resume({
911
+ resumeSessionId: SessionId2(sessionId),
912
+ agentOptions: agentOptionsFor(config.model),
913
+ ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
914
+ });
915
+ const cwd = options.cwd ?? storedHeader?.cwd;
916
+ const projection = new SessionProjection(replay.contextWindow, {
917
+ terminalOutput: clientTerminalOutput,
918
+ ...cwd !== void 0 ? { cwd } : {}
919
+ });
920
+ projection.title = replay.title;
921
+ const record = registerRecord(
922
+ sessionId,
923
+ handle.agent,
924
+ () => handle.dispose(),
925
+ config.model,
926
+ config.permissionMode ?? "workspace-write",
927
+ projection
928
+ );
929
+ if (presetId !== void 0) record.preset = presetId;
930
+ {
931
+ const permission = permissionService();
932
+ const storedMode = permission?.current(events);
933
+ if (storedMode !== void 0) record.modeId = storedMode;
934
+ for (let index = events.length - 1; index >= 0; index -= 1) {
935
+ const event = events[index];
936
+ if (event?.type === "approval/policy") {
937
+ const policy = event.data?.policy;
938
+ if (policy === "ask" || policy === "never") record.approvals = policy;
939
+ break;
940
+ }
941
+ }
942
+ }
943
+ ensureSelection(record);
944
+ queueMicrotask(() => publishCommands(sessionId));
945
+ return record;
946
+ };
866
947
  const assertOpen = () => {
867
948
  if (closed) throw internalError("the ACP bridge has been disposed");
868
949
  };
@@ -964,7 +1045,7 @@ async function apply(ctx, config = {}) {
964
1045
  }
965
1046
  record.approvals = policy;
966
1047
  };
967
- const permissionService = () => ctx.get("permission");
1048
+ const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
968
1049
  const applyMode = (record, sessionId, modeId) => {
969
1050
  const permission = permissionService();
970
1051
  if (permission !== void 0 && permission.names.includes(modeId)) {
@@ -1093,7 +1174,12 @@ async function apply(ctx, config = {}) {
1093
1174
  description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1094
1175
  input: { hint: "<api-key>" }
1095
1176
  },
1096
- { name: "logout", description: "Remove the API key stored in the harness credential store" }
1177
+ { name: "logout", description: "Remove the API key stored in the harness credential store" },
1178
+ {
1179
+ name: "model",
1180
+ description: "Select the model for this conversation",
1181
+ input: { hint: "[model] \u2014 blank lists available models" }
1182
+ }
1097
1183
  ];
1098
1184
  const availableCommandsFor = async (record) => {
1099
1185
  const list = [...BUILTIN_COMMANDS];
@@ -1146,6 +1232,37 @@ async function apply(ctx, config = {}) {
1146
1232
  logWarn(`publishing commands failed: ${String(error)}`);
1147
1233
  });
1148
1234
  };
1235
+ const publishConfigOptions = (sessionId, record) => {
1236
+ void configOptions(record).then((options) => {
1237
+ notify(sessionId, {
1238
+ sessionUpdate: "config_option_update",
1239
+ configOptions: options
1240
+ });
1241
+ }).catch((error) => {
1242
+ logWarn(`publishing config options failed: ${String(error)}`);
1243
+ });
1244
+ };
1245
+ const syncPermissionState = (record, sessionId) => {
1246
+ const events = record.agent.session.events;
1247
+ if (events === void 0) return;
1248
+ const permission = permissionService();
1249
+ const storedMode = permission?.current(events);
1250
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1251
+ const event = events[index];
1252
+ if (event?.type === "approval/policy") {
1253
+ const policy = event.data?.policy;
1254
+ if (policy === "ask" || policy === "never") record.approvals = policy;
1255
+ break;
1256
+ }
1257
+ }
1258
+ if (storedMode !== void 0 && storedMode !== record.modeId) {
1259
+ record.modeId = storedMode;
1260
+ notify(sessionId, {
1261
+ sessionUpdate: "current_mode_update",
1262
+ currentModeId: record.modeId
1263
+ });
1264
+ }
1265
+ };
1149
1266
  const credentialStore = () => {
1150
1267
  const brand = harness.credentialRef;
1151
1268
  const store = ctx.get("credentials");
@@ -1210,6 +1327,49 @@ async function apply(ctx, config = {}) {
1210
1327
  const remaining = await describeCredential();
1211
1328
  return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1212
1329
  };
1330
+ const modelCommandText = async (record, acpSessionId, query) => {
1331
+ const discovered = await discoverModels();
1332
+ const catalog = [
1333
+ ...discovered.map((entry) => ({
1334
+ value: encodeChoice(entry.provider, entry.model),
1335
+ label: entry.label
1336
+ })),
1337
+ ...modelCandidates().filter((model) => !discovered.some((entry) => entry.model === model)).map((model) => ({ value: encodeChoice(void 0, model), label: model }))
1338
+ ];
1339
+ const route = routeOf(record);
1340
+ const currentValue = encodeChoice(record.provider, record.model ?? route.model ?? "");
1341
+ if (query === "") {
1342
+ const lines = catalog.map(
1343
+ (entry) => `${entry.value === currentValue ? "\u2192" : " "} ${entry.label} \u2014 ${entry.value}`
1344
+ );
1345
+ return [
1346
+ `model: ${route.model ?? "(product default)"}${record.effort !== void 0 ? ` (effort ${record.effort})` : ""}`,
1347
+ "",
1348
+ ...lines.length > 0 ? lines : ["no models discovered \u2014 check credentials with /status"],
1349
+ "",
1350
+ "switch with /model <name>"
1351
+ ].join("\n");
1352
+ }
1353
+ const lowered = query.toLowerCase();
1354
+ const exact = catalog.filter(
1355
+ (entry) => entry.value.toLowerCase() === lowered || decodeChoice(entry.value).model.toLowerCase() === lowered
1356
+ );
1357
+ const matches = exact.length > 0 ? exact : catalog.filter(
1358
+ (entry) => entry.value.toLowerCase().includes(lowered) || entry.label.toLowerCase().includes(lowered)
1359
+ );
1360
+ if (matches.length === 0) return `no model matches "${query}" \u2014 see /model`;
1361
+ if (matches.length > 1) {
1362
+ return [`"${query}" is ambiguous:`, ...matches.map((entry) => ` ${entry.label} \u2014 ${entry.value}`)].join(
1363
+ "\n"
1364
+ );
1365
+ }
1366
+ const target = matches[0];
1367
+ if (target.value === currentValue) return `already on ${target.label}`;
1368
+ await switchModel(record, acpSessionId, target.value);
1369
+ publishCommands(acpSessionId, record);
1370
+ publishConfigOptions(acpSessionId, record);
1371
+ return `model \u2192 ${target.label}`;
1372
+ };
1213
1373
  const statusText = async (record) => {
1214
1374
  const used = record.projection.contextWindow;
1215
1375
  const route = routeOf(record);
@@ -1322,13 +1482,24 @@ async function apply(ctx, config = {}) {
1322
1482
  mcpCapabilities: { http: true, sse: false },
1323
1483
  ...hasPersistence ? { sessionCapabilities: { list: {} } } : {}
1324
1484
  },
1325
- authMethods: []
1485
+ authMethods: [
1486
+ // ACP Terminal Auth (see the registry's AUTHENTICATION.md):
1487
+ // the client runs `dsh-acp login` in an interactive
1488
+ // terminal; the key lands in the shared harness
1489
+ // credential store (~/.dsh/.credentials.yaml).
1490
+ {
1491
+ id: "terminal-login",
1492
+ name: "Log in with a DeepSeek API key",
1493
+ description: "Interactive terminal setup \u2014 saves the key to the harness credential store shared with the dsh Web UI",
1494
+ _meta: { "terminal-auth": { args: ["login"], env: {} } }
1495
+ }
1496
+ ]
1326
1497
  });
1327
1498
  },
1328
1499
  async authenticate(_params) {
1329
1500
  if (!await credentialPresent()) {
1330
1501
  throw authRequired(
1331
- "no DeepSeek credential found: save one in the dsh Web UI (Settings \u2192 Models) or set DEEPSEEK_API_KEY"
1502
+ "no DeepSeek credential found: run `dsh-acp login`, save one in the dsh Web UI (Settings \u2192 Models), or set DEEPSEEK_API_KEY"
1332
1503
  );
1333
1504
  }
1334
1505
  },
@@ -1370,68 +1541,8 @@ async function apply(ctx, config = {}) {
1370
1541
  assertOpen();
1371
1542
  validateCwd(params.cwd);
1372
1543
  syncMcpServers(params.mcpServers, params.cwd);
1373
- const persistence = requirePersistence();
1374
- const sessionId = params.sessionId;
1375
- const existing = sessions.get(sessionId);
1376
- if (existing !== void 0) {
1377
- sessions.delete(sessionId);
1378
- existing.agent.cancel({ kind: "user" });
1379
- settlePrompt(existing, "cancelled");
1380
- await existing.dispose().catch((error) => {
1381
- logWarn(`dispose before reload failed: ${String(error)}`);
1382
- });
1383
- }
1384
- let events;
1385
- let storedHeader;
1386
- try {
1387
- const inspected = await persistence.inspect(SessionId2(sessionId));
1388
- events = inspected.events;
1389
- storedHeader = inspected.header;
1390
- } catch (error) {
1391
- throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
1392
- }
1393
- const replay = buildReplay(events);
1394
- for (const update of replay.updates) notify(sessionId, update);
1395
- if (replay.title !== void 0) {
1396
- notify(sessionId, {
1397
- sessionUpdate: "session_info_update",
1398
- title: replay.title
1399
- });
1400
- }
1401
- const presets = presetsService();
1402
- const storedPreset = presetFromLog(storedHeader, events);
1403
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1404
- const handle = await agents.resume({
1405
- resumeSessionId: SessionId2(sessionId),
1406
- agentOptions: agentOptionsFor(config.model),
1407
- ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1408
- });
1409
- const projection = new SessionProjection(replay.contextWindow);
1410
- projection.title = replay.title;
1411
- const record = registerRecord(
1412
- sessionId,
1413
- handle.agent,
1414
- () => handle.dispose(),
1415
- config.model,
1416
- config.permissionMode ?? "workspace-write",
1417
- projection
1418
- );
1419
- if (presetId !== void 0) record.preset = presetId;
1420
- {
1421
- const permission = permissionService();
1422
- const storedMode = permission?.current(events);
1423
- if (storedMode !== void 0) record.modeId = storedMode;
1424
- for (let index = events.length - 1; index >= 0; index -= 1) {
1425
- const event = events[index];
1426
- if (event?.type === "approval/policy") {
1427
- const policy = event.data?.policy;
1428
- if (policy === "ask" || policy === "never") record.approvals = policy;
1429
- break;
1430
- }
1431
- }
1432
- }
1433
- ensureSelection(record);
1434
- queueMicrotask(() => publishCommands(sessionId));
1544
+ requirePersistence();
1545
+ const record = await restoreSession(params.sessionId, { replay: true, cwd: params.cwd });
1435
1546
  const options = await configOptions(record);
1436
1547
  return {
1437
1548
  modes: modeState(record),
@@ -1465,7 +1576,7 @@ async function apply(ctx, config = {}) {
1465
1576
  },
1466
1577
  async prompt(params) {
1467
1578
  assertOpen();
1468
- const record = requireSession(params.sessionId);
1579
+ const record = await requireOrRestoreSession(params.sessionId);
1469
1580
  if (record.inflight !== void 0) {
1470
1581
  throw invalidParams("a prompt is already in flight for this session");
1471
1582
  }
@@ -1491,6 +1602,15 @@ async function apply(ctx, config = {}) {
1491
1602
  return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1492
1603
  }
1493
1604
  if (commandMatch?.[1] === "logout") return respond(await logoutText());
1605
+ if (commandMatch?.[1] === "model") {
1606
+ return respond(
1607
+ await modelCommandText(
1608
+ record,
1609
+ params.sessionId,
1610
+ trimmed.slice(commandMatch[0].length).trim()
1611
+ )
1612
+ );
1613
+ }
1494
1614
  if (commandMatch !== null && commandMatch[1] !== void 0) {
1495
1615
  const commandsSvc = ctx.get("commands");
1496
1616
  if (commandsSvc !== void 0) {
@@ -1507,7 +1627,9 @@ async function apply(ctx, config = {}) {
1507
1627
  if (execution !== void 0) {
1508
1628
  const { result } = execution;
1509
1629
  const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1630
+ syncPermissionState(record, params.sessionId);
1510
1631
  publishCommands(params.sessionId, record);
1632
+ publishConfigOptions(params.sessionId, record);
1511
1633
  return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1512
1634
  }
1513
1635
  }
@@ -1558,7 +1680,7 @@ async function apply(ctx, config = {}) {
1558
1680
  return { stopReason, ...usage !== void 0 ? { usage } : {} };
1559
1681
  },
1560
1682
  async setSessionMode(params) {
1561
- const record = requireSession(params.sessionId);
1683
+ const record = await requireOrRestoreSession(params.sessionId);
1562
1684
  applyMode(record, params.sessionId, params.modeId);
1563
1685
  notify(params.sessionId, {
1564
1686
  sessionUpdate: "config_option_update",
@@ -1567,7 +1689,7 @@ async function apply(ctx, config = {}) {
1567
1689
  return {};
1568
1690
  },
1569
1691
  async setSessionConfigOption(params) {
1570
- const record = requireSession(params.sessionId);
1692
+ const record = await requireOrRestoreSession(params.sessionId);
1571
1693
  const value = typeof params.value === "string" ? params.value : void 0;
1572
1694
  if (value === void 0) throw invalidParams(`invalid value: ${String(params.value)}`);
1573
1695
  switch (params.configId) {
package/dist/index.js CHANGED
@@ -888,6 +888,87 @@ async function apply(ctx, config = {}) {
888
888
  if (record === void 0) throw invalidParams(`unknown session: ${sessionId}`);
889
889
  return record;
890
890
  };
891
+ const requireOrRestoreSession = async (sessionId) => {
892
+ const record = sessions.get(sessionId);
893
+ if (record !== void 0) return record;
894
+ if (ctx.get("sessionPersistence") === void 0) throw invalidParams(`unknown session: ${sessionId}`);
895
+ logWarn(`restoring session ${sessionId} from the persisted log`);
896
+ try {
897
+ return await restoreSession(sessionId, { replay: false });
898
+ } catch (error) {
899
+ throw invalidParams(`unknown session: ${sessionId} (${errorChain2(error)})`);
900
+ }
901
+ };
902
+ const restoreSession = async (sessionId, options) => {
903
+ const persistence = requirePersistence();
904
+ const existing = sessions.get(sessionId);
905
+ if (existing !== void 0) {
906
+ sessions.delete(sessionId);
907
+ existing.agent.cancel({ kind: "user" });
908
+ settlePrompt(existing, "cancelled");
909
+ await existing.dispose().catch((error) => {
910
+ logWarn(`dispose before reload failed: ${String(error)}`);
911
+ });
912
+ }
913
+ let events;
914
+ let storedHeader;
915
+ try {
916
+ const inspected = await persistence.inspect(SessionId2(sessionId));
917
+ events = inspected.events;
918
+ storedHeader = inspected.header;
919
+ } catch (error) {
920
+ throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
921
+ }
922
+ const replay = buildReplay(events);
923
+ if (options.replay) {
924
+ for (const update of replay.updates) notify(sessionId, update);
925
+ if (replay.title !== void 0) {
926
+ notify(sessionId, {
927
+ sessionUpdate: "session_info_update",
928
+ title: replay.title
929
+ });
930
+ }
931
+ }
932
+ const presets = presetsService();
933
+ const storedPreset = presetFromLog(storedHeader, events);
934
+ const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
935
+ const handle = await agents.resume({
936
+ resumeSessionId: SessionId2(sessionId),
937
+ agentOptions: agentOptionsFor(config.model),
938
+ ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
939
+ });
940
+ const cwd = options.cwd ?? storedHeader?.cwd;
941
+ const projection = new SessionProjection(replay.contextWindow, {
942
+ terminalOutput: clientTerminalOutput,
943
+ ...cwd !== void 0 ? { cwd } : {}
944
+ });
945
+ projection.title = replay.title;
946
+ const record = registerRecord(
947
+ sessionId,
948
+ handle.agent,
949
+ () => handle.dispose(),
950
+ config.model,
951
+ config.permissionMode ?? "workspace-write",
952
+ projection
953
+ );
954
+ if (presetId !== void 0) record.preset = presetId;
955
+ {
956
+ const permission = permissionService();
957
+ const storedMode = permission?.current(events);
958
+ if (storedMode !== void 0) record.modeId = storedMode;
959
+ for (let index = events.length - 1; index >= 0; index -= 1) {
960
+ const event = events[index];
961
+ if (event?.type === "approval/policy") {
962
+ const policy = event.data?.policy;
963
+ if (policy === "ask" || policy === "never") record.approvals = policy;
964
+ break;
965
+ }
966
+ }
967
+ }
968
+ ensureSelection(record);
969
+ queueMicrotask(() => publishCommands(sessionId));
970
+ return record;
971
+ };
891
972
  const assertOpen = () => {
892
973
  if (closed) throw internalError("the ACP bridge has been disposed");
893
974
  };
@@ -989,7 +1070,7 @@ async function apply(ctx, config = {}) {
989
1070
  }
990
1071
  record.approvals = policy;
991
1072
  };
992
- const permissionService = () => ctx.get("permission");
1073
+ const permissionService = () => ctx.get("permissionPresets") ?? ctx.get("permission");
993
1074
  const applyMode = (record, sessionId, modeId) => {
994
1075
  const permission = permissionService();
995
1076
  if (permission !== void 0 && permission.names.includes(modeId)) {
@@ -1118,7 +1199,12 @@ async function apply(ctx, config = {}) {
1118
1199
  description: "Save a DeepSeek API key into the harness credential store (~/.dsh/.credentials.yaml)",
1119
1200
  input: { hint: "<api-key>" }
1120
1201
  },
1121
- { name: "logout", description: "Remove the API key stored in the harness credential store" }
1202
+ { name: "logout", description: "Remove the API key stored in the harness credential store" },
1203
+ {
1204
+ name: "model",
1205
+ description: "Select the model for this conversation",
1206
+ input: { hint: "[model] \u2014 blank lists available models" }
1207
+ }
1122
1208
  ];
1123
1209
  const availableCommandsFor = async (record) => {
1124
1210
  const list = [...BUILTIN_COMMANDS];
@@ -1171,6 +1257,37 @@ async function apply(ctx, config = {}) {
1171
1257
  logWarn(`publishing commands failed: ${String(error)}`);
1172
1258
  });
1173
1259
  };
1260
+ const publishConfigOptions = (sessionId, record) => {
1261
+ void configOptions(record).then((options) => {
1262
+ notify(sessionId, {
1263
+ sessionUpdate: "config_option_update",
1264
+ configOptions: options
1265
+ });
1266
+ }).catch((error) => {
1267
+ logWarn(`publishing config options failed: ${String(error)}`);
1268
+ });
1269
+ };
1270
+ const syncPermissionState = (record, sessionId) => {
1271
+ const events = record.agent.session.events;
1272
+ if (events === void 0) return;
1273
+ const permission = permissionService();
1274
+ const storedMode = permission?.current(events);
1275
+ for (let index = events.length - 1; index >= 0; index -= 1) {
1276
+ const event = events[index];
1277
+ if (event?.type === "approval/policy") {
1278
+ const policy = event.data?.policy;
1279
+ if (policy === "ask" || policy === "never") record.approvals = policy;
1280
+ break;
1281
+ }
1282
+ }
1283
+ if (storedMode !== void 0 && storedMode !== record.modeId) {
1284
+ record.modeId = storedMode;
1285
+ notify(sessionId, {
1286
+ sessionUpdate: "current_mode_update",
1287
+ currentModeId: record.modeId
1288
+ });
1289
+ }
1290
+ };
1174
1291
  const credentialStore = () => {
1175
1292
  const brand = harness.credentialRef;
1176
1293
  const store = ctx.get("credentials");
@@ -1235,6 +1352,49 @@ async function apply(ctx, config = {}) {
1235
1352
  const remaining = await describeCredential();
1236
1353
  return remaining === "not configured" ? "Removed the stored DEEPSEEK_API_KEY. No credential is configured now." : `Removed the stored DEEPSEEK_API_KEY; requests now authenticate via: ${remaining}.`;
1237
1354
  };
1355
+ const modelCommandText = async (record, acpSessionId, query) => {
1356
+ const discovered = await discoverModels();
1357
+ const catalog = [
1358
+ ...discovered.map((entry) => ({
1359
+ value: encodeChoice(entry.provider, entry.model),
1360
+ label: entry.label
1361
+ })),
1362
+ ...modelCandidates().filter((model) => !discovered.some((entry) => entry.model === model)).map((model) => ({ value: encodeChoice(void 0, model), label: model }))
1363
+ ];
1364
+ const route = routeOf(record);
1365
+ const currentValue = encodeChoice(record.provider, record.model ?? route.model ?? "");
1366
+ if (query === "") {
1367
+ const lines = catalog.map(
1368
+ (entry) => `${entry.value === currentValue ? "\u2192" : " "} ${entry.label} \u2014 ${entry.value}`
1369
+ );
1370
+ return [
1371
+ `model: ${route.model ?? "(product default)"}${record.effort !== void 0 ? ` (effort ${record.effort})` : ""}`,
1372
+ "",
1373
+ ...lines.length > 0 ? lines : ["no models discovered \u2014 check credentials with /status"],
1374
+ "",
1375
+ "switch with /model <name>"
1376
+ ].join("\n");
1377
+ }
1378
+ const lowered = query.toLowerCase();
1379
+ const exact = catalog.filter(
1380
+ (entry) => entry.value.toLowerCase() === lowered || decodeChoice(entry.value).model.toLowerCase() === lowered
1381
+ );
1382
+ const matches = exact.length > 0 ? exact : catalog.filter(
1383
+ (entry) => entry.value.toLowerCase().includes(lowered) || entry.label.toLowerCase().includes(lowered)
1384
+ );
1385
+ if (matches.length === 0) return `no model matches "${query}" \u2014 see /model`;
1386
+ if (matches.length > 1) {
1387
+ return [`"${query}" is ambiguous:`, ...matches.map((entry) => ` ${entry.label} \u2014 ${entry.value}`)].join(
1388
+ "\n"
1389
+ );
1390
+ }
1391
+ const target = matches[0];
1392
+ if (target.value === currentValue) return `already on ${target.label}`;
1393
+ await switchModel(record, acpSessionId, target.value);
1394
+ publishCommands(acpSessionId, record);
1395
+ publishConfigOptions(acpSessionId, record);
1396
+ return `model \u2192 ${target.label}`;
1397
+ };
1238
1398
  const statusText = async (record) => {
1239
1399
  const used = record.projection.contextWindow;
1240
1400
  const route = routeOf(record);
@@ -1347,13 +1507,24 @@ async function apply(ctx, config = {}) {
1347
1507
  mcpCapabilities: { http: true, sse: false },
1348
1508
  ...hasPersistence ? { sessionCapabilities: { list: {} } } : {}
1349
1509
  },
1350
- authMethods: []
1510
+ authMethods: [
1511
+ // ACP Terminal Auth (see the registry's AUTHENTICATION.md):
1512
+ // the client runs `dsh-acp login` in an interactive
1513
+ // terminal; the key lands in the shared harness
1514
+ // credential store (~/.dsh/.credentials.yaml).
1515
+ {
1516
+ id: "terminal-login",
1517
+ name: "Log in with a DeepSeek API key",
1518
+ description: "Interactive terminal setup \u2014 saves the key to the harness credential store shared with the dsh Web UI",
1519
+ _meta: { "terminal-auth": { args: ["login"], env: {} } }
1520
+ }
1521
+ ]
1351
1522
  });
1352
1523
  },
1353
1524
  async authenticate(_params) {
1354
1525
  if (!await credentialPresent()) {
1355
1526
  throw authRequired(
1356
- "no DeepSeek credential found: save one in the dsh Web UI (Settings \u2192 Models) or set DEEPSEEK_API_KEY"
1527
+ "no DeepSeek credential found: run `dsh-acp login`, save one in the dsh Web UI (Settings \u2192 Models), or set DEEPSEEK_API_KEY"
1357
1528
  );
1358
1529
  }
1359
1530
  },
@@ -1395,68 +1566,8 @@ async function apply(ctx, config = {}) {
1395
1566
  assertOpen();
1396
1567
  validateCwd(params.cwd);
1397
1568
  syncMcpServers(params.mcpServers, params.cwd);
1398
- const persistence = requirePersistence();
1399
- const sessionId = params.sessionId;
1400
- const existing = sessions.get(sessionId);
1401
- if (existing !== void 0) {
1402
- sessions.delete(sessionId);
1403
- existing.agent.cancel({ kind: "user" });
1404
- settlePrompt(existing, "cancelled");
1405
- await existing.dispose().catch((error) => {
1406
- logWarn(`dispose before reload failed: ${String(error)}`);
1407
- });
1408
- }
1409
- let events;
1410
- let storedHeader;
1411
- try {
1412
- const inspected = await persistence.inspect(SessionId2(sessionId));
1413
- events = inspected.events;
1414
- storedHeader = inspected.header;
1415
- } catch (error) {
1416
- throw invalidParams(`session not found: ${sessionId} (${errorChain2(error)})`);
1417
- }
1418
- const replay = buildReplay(events);
1419
- for (const update of replay.updates) notify(sessionId, update);
1420
- if (replay.title !== void 0) {
1421
- notify(sessionId, {
1422
- sessionUpdate: "session_info_update",
1423
- title: replay.title
1424
- });
1425
- }
1426
- const presets = presetsService();
1427
- const storedPreset = presetFromLog(storedHeader, events);
1428
- const presetId = presets === void 0 ? void 0 : (await presets.resolve(storedPreset)).id;
1429
- const handle = await agents.resume({
1430
- resumeSessionId: SessionId2(sessionId),
1431
- agentOptions: agentOptionsFor(config.model),
1432
- ...presetSetup(presets, presetId) !== void 0 ? { setup: presetSetup(presets, presetId) } : {}
1433
- });
1434
- const projection = new SessionProjection(replay.contextWindow);
1435
- projection.title = replay.title;
1436
- const record = registerRecord(
1437
- sessionId,
1438
- handle.agent,
1439
- () => handle.dispose(),
1440
- config.model,
1441
- config.permissionMode ?? "workspace-write",
1442
- projection
1443
- );
1444
- if (presetId !== void 0) record.preset = presetId;
1445
- {
1446
- const permission = permissionService();
1447
- const storedMode = permission?.current(events);
1448
- if (storedMode !== void 0) record.modeId = storedMode;
1449
- for (let index = events.length - 1; index >= 0; index -= 1) {
1450
- const event = events[index];
1451
- if (event?.type === "approval/policy") {
1452
- const policy = event.data?.policy;
1453
- if (policy === "ask" || policy === "never") record.approvals = policy;
1454
- break;
1455
- }
1456
- }
1457
- }
1458
- ensureSelection(record);
1459
- queueMicrotask(() => publishCommands(sessionId));
1569
+ requirePersistence();
1570
+ const record = await restoreSession(params.sessionId, { replay: true, cwd: params.cwd });
1460
1571
  const options = await configOptions(record);
1461
1572
  return {
1462
1573
  modes: modeState(record),
@@ -1490,7 +1601,7 @@ async function apply(ctx, config = {}) {
1490
1601
  },
1491
1602
  async prompt(params) {
1492
1603
  assertOpen();
1493
- const record = requireSession(params.sessionId);
1604
+ const record = await requireOrRestoreSession(params.sessionId);
1494
1605
  if (record.inflight !== void 0) {
1495
1606
  throw invalidParams("a prompt is already in flight for this session");
1496
1607
  }
@@ -1516,6 +1627,15 @@ async function apply(ctx, config = {}) {
1516
1627
  return respond(await loginText(trimmed.slice(commandMatch[0].length)));
1517
1628
  }
1518
1629
  if (commandMatch?.[1] === "logout") return respond(await logoutText());
1630
+ if (commandMatch?.[1] === "model") {
1631
+ return respond(
1632
+ await modelCommandText(
1633
+ record,
1634
+ params.sessionId,
1635
+ trimmed.slice(commandMatch[0].length).trim()
1636
+ )
1637
+ );
1638
+ }
1519
1639
  if (commandMatch !== null && commandMatch[1] !== void 0) {
1520
1640
  const commandsSvc = ctx.get("commands");
1521
1641
  if (commandsSvc !== void 0) {
@@ -1532,7 +1652,9 @@ async function apply(ctx, config = {}) {
1532
1652
  if (execution !== void 0) {
1533
1653
  const { result } = execution;
1534
1654
  const text = result.text ?? (result.kind === "success" ? `/${commandMatch[1]} \u2713` : `/${commandMatch[1]} failed`);
1655
+ syncPermissionState(record, params.sessionId);
1535
1656
  publishCommands(params.sessionId, record);
1657
+ publishConfigOptions(params.sessionId, record);
1536
1658
  return respond(result.kind === "error" ? `\u26A0 ${text}` : text);
1537
1659
  }
1538
1660
  }
@@ -1583,7 +1705,7 @@ async function apply(ctx, config = {}) {
1583
1705
  return { stopReason, ...usage !== void 0 ? { usage } : {} };
1584
1706
  },
1585
1707
  async setSessionMode(params) {
1586
- const record = requireSession(params.sessionId);
1708
+ const record = await requireOrRestoreSession(params.sessionId);
1587
1709
  applyMode(record, params.sessionId, params.modeId);
1588
1710
  notify(params.sessionId, {
1589
1711
  sessionUpdate: "config_option_update",
@@ -1592,7 +1714,7 @@ async function apply(ctx, config = {}) {
1592
1714
  return {};
1593
1715
  },
1594
1716
  async setSessionConfigOption(params) {
1595
- const record = requireSession(params.sessionId);
1717
+ const record = await requireOrRestoreSession(params.sessionId);
1596
1718
  const value = typeof params.value === "string" ? params.value : void 0;
1597
1719
  if (value === void 0) throw invalidParams(`invalid value: ${String(params.value)}`);
1598
1720
  switch (params.configId) {
@@ -2245,6 +2367,9 @@ async function bootAcpProfile(host, overrides) {
2245
2367
  overlays.push({ id: "session-telemetry-otel", disabled: true });
2246
2368
  }
2247
2369
  const bridgeRow = rows.get("acp-bridge");
2370
+ if (overrides?.serve === false && bridgeRow !== void 0) {
2371
+ overlays.push({ id: "acp-bridge", disabled: true });
2372
+ }
2248
2373
  if (overrides !== void 0 && bridgeRow !== void 0) {
2249
2374
  const patch = {};
2250
2375
  if (overrides.provider !== void 0) patch["provider"] = overrides.provider;
@@ -2281,9 +2406,93 @@ async function bootAcpProfile(host, overrides) {
2281
2406
  return ctx;
2282
2407
  }
2283
2408
 
2409
+ // src/login.ts
2410
+ import { createRequire as createRequire3 } from "node:module";
2411
+ import { join as join3 } from "node:path";
2412
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
2413
+ async function readSecret(promptText) {
2414
+ const stdin = process.stdin;
2415
+ if (!stdin.isTTY) {
2416
+ const chunks = [];
2417
+ for await (const chunk of stdin) {
2418
+ chunks.push(Buffer.from(chunk));
2419
+ const text = Buffer.concat(chunks).toString("utf8");
2420
+ const newline = text.indexOf("\n");
2421
+ if (newline >= 0) return text.slice(0, newline).trim();
2422
+ }
2423
+ return Buffer.concat(chunks).toString("utf8").trim();
2424
+ }
2425
+ process.stderr.write(promptText);
2426
+ return await new Promise((resolve2) => {
2427
+ stdin.setRawMode(true);
2428
+ stdin.resume();
2429
+ stdin.setEncoding("utf8");
2430
+ let buffer = "";
2431
+ const onData = (chunk) => {
2432
+ for (const char of chunk) {
2433
+ if (char === "\n" || char === "\r" || char === "") {
2434
+ stdin.setRawMode(false);
2435
+ stdin.pause();
2436
+ stdin.off("data", onData);
2437
+ process.stderr.write("\n");
2438
+ resolve2(buffer.trim());
2439
+ return;
2440
+ }
2441
+ if (char === "") {
2442
+ stdin.setRawMode(false);
2443
+ process.stderr.write("^C\n");
2444
+ process.exit(130);
2445
+ }
2446
+ if (char === "\x7F" || char === "\b") {
2447
+ buffer = buffer.slice(0, -1);
2448
+ continue;
2449
+ }
2450
+ buffer += char;
2451
+ }
2452
+ };
2453
+ stdin.on("data", onData);
2454
+ });
2455
+ }
2456
+ async function runLogin(ctx, hostBase, argv) {
2457
+ const req = createRequire3(join3(hostBase, "noop.js"));
2458
+ let credentialRef2;
2459
+ try {
2460
+ const mod = await import(pathToFileURL3(req.resolve("@deepseek-ai/dsh-credentials")).href);
2461
+ credentialRef2 = mod.credentialRef;
2462
+ } catch {
2463
+ credentialRef2 = void 0;
2464
+ }
2465
+ const store = ctx.get("credentials");
2466
+ if (store === void 0 || credentialRef2 === void 0) {
2467
+ process.stderr.write(
2468
+ "This composition has no writable credential store; export DEEPSEEK_API_KEY instead.\n"
2469
+ );
2470
+ return 1;
2471
+ }
2472
+ const ref = credentialRef2("DEEPSEEK_API_KEY");
2473
+ const before = await store.describe(ref);
2474
+ if (before.configured && before.source !== void 0) {
2475
+ process.stderr.write(`A key is already configured (source: ${before.source}).
2476
+ `);
2477
+ }
2478
+ const key = argv[0] ?? await readSecret("DeepSeek API key (input hidden): ");
2479
+ if (key.length === 0) {
2480
+ process.stderr.write("No key entered; nothing saved.\n");
2481
+ return 1;
2482
+ }
2483
+ await store.set(ref, key);
2484
+ const masked = key.length <= 8 ? "\u2026" : `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
2485
+ process.stderr.write(
2486
+ `Saved ${masked} to the harness credential store (~/.dsh/.credentials.yaml, mode 600).
2487
+ The dsh Web UI and every dsh surface share this credential.
2488
+ `
2489
+ );
2490
+ return 0;
2491
+ }
2492
+
2284
2493
  // src/settings.ts
2285
2494
  import { homedir } from "node:os";
2286
- import { join as join3 } from "node:path";
2495
+ import { join as join4 } from "node:path";
2287
2496
  var DEFAULT_PERSONA = `You are a coding assistant powered by the {{model}} model, working inside an editor via the Agent Client Protocol. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox \u2014 a \`[sandbox: file access denied \u2026]\` result is policy, not a command bug.
2288
2497
 
2289
2498
  Verify your work by running the code or tests. Keep answers brief and factual.`;
@@ -2376,7 +2585,7 @@ function resolveSettings(argv) {
2376
2585
  models,
2377
2586
  maxTokens,
2378
2587
  permissionMode,
2379
- sessionRoot: stringFlag(parsed, "session-root") ?? envString("DSH_SESSION_ROOT") ?? join3(homedir(), ".dsh-acp", "sessions"),
2588
+ sessionRoot: stringFlag(parsed, "session-root") ?? envString("DSH_SESSION_ROOT") ?? join4(homedir(), ".dsh-acp", "sessions"),
2380
2589
  persona: stringFlag(parsed, "persona") ?? envString("DSH_SYSTEM_PROMPT") ?? DEFAULT_PERSONA,
2381
2590
  thinking: !parsed.flags.has("no-thinking"),
2382
2591
  reasoningEffort: reasoningRaw,
@@ -2384,6 +2593,8 @@ function resolveSettings(argv) {
2384
2593
  };
2385
2594
  }
2386
2595
  var HELP_TEXT = `Usage: dsh-acp [options]
2596
+ dsh-acp login [api-key] Save a DeepSeek API key (interactive when omitted)
2597
+ dsh-acp update Self-update via npm
2387
2598
 
2388
2599
  Agent Client Protocol (ACP) stdio server for DeepSeek Harness. Speak ACP on
2389
2600
  stdin/stdout from an ACP client such as Zed; diagnostics go to stderr.
@@ -2427,6 +2638,32 @@ async function main() {
2427
2638
  console.log(HELP_TEXT.trimEnd());
2428
2639
  return;
2429
2640
  }
2641
+ if (argv[0] === "login") {
2642
+ let booted;
2643
+ let hostBase;
2644
+ try {
2645
+ const host = resolveHost(void 0);
2646
+ hostBase = host.base;
2647
+ booted = await bootAcpProfile(host, { serve: false });
2648
+ } catch (error) {
2649
+ if (error instanceof HarnessNotFoundError) {
2650
+ logError(error.message);
2651
+ process.exitCode = 1;
2652
+ return;
2653
+ }
2654
+ throw error;
2655
+ }
2656
+ try {
2657
+ process.exitCode = await runLogin(
2658
+ booted,
2659
+ hostBase,
2660
+ argv.slice(1)
2661
+ );
2662
+ } finally {
2663
+ await booted.fiber.dispose().catch(() => void 0);
2664
+ }
2665
+ return;
2666
+ }
2430
2667
  if (argv[0] === "update") {
2431
2668
  logInfo(`updating ${NAME} (npm install -g ${NAME}@latest)\u2026`);
2432
2669
  const result = spawnSync("npm", ["install", "-g", `${NAME}@latest`], { stdio: ["ignore", 2, 2] });
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.3",
6
+ "version": "0.4.5",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/openma-ai/deepseek-harness-acp.git"