sentinelayer-cli 0.30.12 → 0.30.14

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/README.md CHANGED
@@ -658,14 +658,19 @@ The CLI now includes deterministic MCP registry commands:
658
658
  - `sl mcp schema write`
659
659
  - `sl mcp registry init-aidenid`
660
660
  - `sl mcp registry init-aidenid-adapter`
661
+ - `sl mcp registry init-session`
662
+ - `sl mcp registry init-cli`
661
663
  - `sl mcp registry validate --file <path>`
662
664
  - `sl mcp registry validate-aidenid-adapter --file <path> [--registry-file <path>]`
663
665
  - `sl mcp server init --id <server-id> --registry-file <path>`
664
666
  - `sl mcp server validate --file <path>`
667
+ - `sl mcp server run --path .`
665
668
  - `sl mcp bridge init-vscode --server-id <server-id> --server-config <path>`
666
669
 
667
670
  Use `init-aidenid` to scaffold an Anthropic-compatible tool schema wrapper for AIdenID provisioning APIs, then customize transport/auth before runtime wiring.
668
671
  Use `init-aidenid-adapter` to scaffold a deterministic AIdenID provisioning API contract (tool binding -> HTTP path/method -> response field mapping) and cross-check it against the registry with `validate-aidenid-adapter`.
672
+ Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, file locks, and attention requests to local MCP clients. Hosted Claude-web/ChatGPT connectors need a separate HTTPS/OAuth service and are not shipped by the local stdio server. See [docs/mcp.md](docs/mcp.md).
673
+ Use `init-cli` to generate a bridge registry for every `sl` leaf command from Commander metadata. This exposes the full CLI surface as `sl.<command.path>` tool schemas, but execution still requires a bridge-capable MCP host with OAuth/session-seat policy and human approval enforcement. The hosted connector target contract is documented in [docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md).
669
674
 
670
675
  ## Plugin governance foundation (Phase 5.2 slice)
671
676
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.30.12",
3
+ "version": "0.30.14",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -24,6 +24,7 @@ import {
24
24
  validateMcpToolRegistry,
25
25
  writeJsonFile,
26
26
  } from "../mcp/registry.js";
27
+ import { buildSentinelayerCliRegistryTemplate } from "../mcp/cli-registry.js";
27
28
  import { runMcpStdioServer } from "../mcp/session-stdio-server.js";
28
29
  import { resolveOutputRoot } from "../config/service.js";
29
30
 
@@ -254,6 +255,39 @@ export function registerMcpCommand(program) {
254
255
  console.log(pc.gray("Next: run 'sl mcp server init --id sentinelayer-session --registry-file <path>'."));
255
256
  });
256
257
 
258
+ registry
259
+ .command("init-cli")
260
+ .description("Write a generated MCP registry for every SentinelLayer CLI leaf command")
261
+ .option("--path <path>", "Destination file path override")
262
+ .option("--output-dir <path>", "Optional artifact output root override")
263
+ .option("--force", "Overwrite destination file if it already exists")
264
+ .option("--json", "Emit machine-readable output")
265
+ .action(async (options, command) => {
266
+ const defaultPath = await resolveDefaultMcpOutputPath({
267
+ cwd: process.cwd(),
268
+ outputDir: options.outputDir,
269
+ env: process.env,
270
+ });
271
+ const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", ".cli-tools"));
272
+ const template = await buildSentinelayerCliRegistryTemplate();
273
+ validateMcpToolRegistry(template);
274
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
275
+ const payload = {
276
+ command: "mcp registry init-cli",
277
+ outputPath: writtenPath,
278
+ toolCount: template.tools.length,
279
+ tools: template.tools.map((tool) => tool.name),
280
+ };
281
+
282
+ if (shouldEmitJson(options, command)) {
283
+ console.log(JSON.stringify(payload, null, 2));
284
+ return;
285
+ }
286
+ console.log(pc.green(`Wrote SentinelLayer CLI MCP registry template: ${writtenPath}`));
287
+ console.log(pc.gray(`${template.tools.length} CLI leaf commands exposed as bridge tools.`));
288
+ console.log(pc.gray("Execution requires a bridge-capable MCP host; local stdio run still exposes session tools."));
289
+ });
290
+
257
291
  registry
258
292
  .command("validate")
259
293
  .description("Validate an MCP tool registry JSON payload against Sentinelayer contract")
@@ -101,6 +101,11 @@ import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
101
101
  import { mergeLiveSources } from "../session/live-source.js";
102
102
  import { listenSessionEvents } from "../session/listener.js";
103
103
  import { SESSION_LIVE_SUCCESS_TIPS } from "../session/coordination-guidance.js";
104
+ import {
105
+ DEFAULT_ROTATING_LOG_MAX_BYTES,
106
+ DEFAULT_ROTATING_LOG_MAX_FILES,
107
+ installRotatingConsoleLog,
108
+ } from "../session/rotating-log.js";
104
109
  import { buildSessionRecap } from "../session/recap.js";
105
110
  import { computeTranscriptStats } from "../session/transcript.js";
106
111
  import { deriveSessionTitle } from "../session/senti-naming.js";
@@ -1811,7 +1816,10 @@ export function shouldPublishListenerPresenceHeartbeat({
1811
1816
  if (elapsedMs >= keepaliveMs) {
1812
1817
  return { publish: true, fingerprint, reason: "keepalive" };
1813
1818
  }
1814
- return { publish: false, fingerprint, reason: elapsedMs < minIntervalMs ? "interval" : "unchanged" };
1819
+ if (elapsedMs >= minIntervalMs) {
1820
+ return { publish: true, fingerprint, reason: "interval" };
1821
+ }
1822
+ return { publish: false, fingerprint, reason: "interval" };
1815
1823
  }
1816
1824
 
1817
1825
  async function publishListenerPresenceEvent({
@@ -4475,6 +4483,17 @@ export function registerSessionCommand(program) {
4475
4483
  "900",
4476
4484
  )
4477
4485
  .option("--no-coaching", "Do not emit periodic in-session success reminders")
4486
+ .option("--log-file <path>", "Also write listener output to a bounded rotating log file")
4487
+ .option(
4488
+ "--log-max-bytes <bytes>",
4489
+ `Rotate --log-file after this many bytes (default ${DEFAULT_ROTATING_LOG_MAX_BYTES})`,
4490
+ String(DEFAULT_ROTATING_LOG_MAX_BYTES),
4491
+ )
4492
+ .option(
4493
+ "--log-max-files <n>",
4494
+ `Total log files to retain including the active file (default ${DEFAULT_ROTATING_LOG_MAX_FILES})`,
4495
+ String(DEFAULT_ROTATING_LOG_MAX_FILES),
4496
+ )
4478
4497
  .action(async (options) => {
4479
4498
  const normalizedSessionId = resolveSessionIdOption(options);
4480
4499
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
@@ -4511,6 +4530,9 @@ export function registerSessionCommand(program) {
4511
4530
  if (!["ndjson", "text"].includes(emitFormat)) {
4512
4531
  throw new Error("--emit must be one of: ndjson, text.");
4513
4532
  }
4533
+ const listenLogPath = normalizeString(options.logFile)
4534
+ ? path.resolve(targetPath, String(options.logFile))
4535
+ : "";
4514
4536
  // Optional wake hook: run a host command on each matched event so the
4515
4537
  // host can resume/wake its agent (the notify->resume bridge).
4516
4538
  const emitWakeNotice = (payload = {}) => {
@@ -4571,6 +4593,14 @@ export function registerSessionCommand(program) {
4571
4593
  if (options.fromNow && options.since !== undefined) {
4572
4594
  throw new Error("Use either --from-now or --since, not both.");
4573
4595
  }
4596
+ const restoreConsoleLog = listenLogPath
4597
+ ? installRotatingConsoleLog({
4598
+ logPath: listenLogPath,
4599
+ maxBytes: parsePositiveInteger(options.logMaxBytes, "log-max-bytes", DEFAULT_ROTATING_LOG_MAX_BYTES),
4600
+ maxFiles: parsePositiveInteger(options.logMaxFiles, "log-max-files", DEFAULT_ROTATING_LOG_MAX_FILES),
4601
+ tee: true,
4602
+ })
4603
+ : null;
4574
4604
  const ac = new AbortController();
4575
4605
  const onSigint = () => ac.abort();
4576
4606
  process.on("SIGINT", onSigint);
@@ -4802,6 +4832,9 @@ export function registerSessionCommand(program) {
4802
4832
  if (coachingTimer) {
4803
4833
  clearInterval(coachingTimer);
4804
4834
  }
4835
+ if (restoreConsoleLog) {
4836
+ restoreConsoleLog();
4837
+ }
4805
4838
  process.removeListener("SIGINT", onSigint);
4806
4839
  }
4807
4840
  });
@@ -4826,6 +4859,17 @@ export function registerSessionCommand(program) {
4826
4859
  .option("--checkpoint-idle <seconds>", "Seconds after latest source event before idle checkpoint attempt (default 600)", "600")
4827
4860
  .option("--no-checkpoints", "Disable durable checkpoint generation")
4828
4861
  .option("--no-checkpoint-closeout", "Skip the final closeout checkpoint when the daemon stops")
4862
+ .option("--log-file <path>", "Also write daemon output to a bounded rotating log file")
4863
+ .option(
4864
+ "--log-max-bytes <bytes>",
4865
+ `Rotate --log-file after this many bytes (default ${DEFAULT_ROTATING_LOG_MAX_BYTES})`,
4866
+ String(DEFAULT_ROTATING_LOG_MAX_BYTES),
4867
+ )
4868
+ .option(
4869
+ "--log-max-files <n>",
4870
+ `Total log files to retain including the active file (default ${DEFAULT_ROTATING_LOG_MAX_FILES})`,
4871
+ String(DEFAULT_ROTATING_LOG_MAX_FILES),
4872
+ )
4829
4873
  .option("--once", "Run one Senti health tick and exit (CI/dogfood smoke)")
4830
4874
  .option("--json", "Emit machine-readable output")
4831
4875
  .action(async (sessionId, options, command) => {
@@ -4840,6 +4884,18 @@ export function registerSessionCommand(program) {
4840
4884
  "tick-interval",
4841
4885
  30,
4842
4886
  );
4887
+ const daemonLogPath = normalizeString(options.logFile)
4888
+ ? path.resolve(targetPath, String(options.logFile))
4889
+ : "";
4890
+ const restoreConsoleLog = daemonLogPath
4891
+ ? installRotatingConsoleLog({
4892
+ logPath: daemonLogPath,
4893
+ maxBytes: parsePositiveInteger(options.logMaxBytes, "log-max-bytes", DEFAULT_ROTATING_LOG_MAX_BYTES),
4894
+ maxFiles: parsePositiveInteger(options.logMaxFiles, "log-max-files", DEFAULT_ROTATING_LOG_MAX_FILES),
4895
+ tee: true,
4896
+ })
4897
+ : null;
4898
+ try {
4843
4899
  // One manager per session: refuse to double-run unless --force.
4844
4900
  // A stale pid file (reboot, hard kill) reads as not-running and is
4845
4901
  // safely overwritten.
@@ -4883,26 +4939,32 @@ export function registerSessionCommand(program) {
4883
4939
  };
4884
4940
 
4885
4941
  if (options.once) {
4886
- const payload = await runTickAndBuildPayload();
4887
- const stopped = await daemon.stop("once_complete");
4888
- payload.running = false;
4889
- payload.stopped = {
4890
- stopped: Boolean(stopped?.stopped),
4891
- reason: stopped?.reason || "once_complete",
4892
- checkpointCloseout: stopped?.checkpointCloseout || null,
4893
- };
4894
- if (emitJson) {
4895
- console.log(JSON.stringify(payload, null, 2));
4942
+ try {
4943
+ const payload = await runTickAndBuildPayload();
4944
+ const stopped = await daemon.stop("once_complete");
4945
+ payload.running = false;
4946
+ payload.stopped = {
4947
+ stopped: Boolean(stopped?.stopped),
4948
+ reason: stopped?.reason || "once_complete",
4949
+ checkpointCloseout: stopped?.checkpointCloseout || null,
4950
+ };
4951
+ if (emitJson) {
4952
+ console.log(JSON.stringify(payload, null, 2));
4953
+ return;
4954
+ }
4955
+ const checkpoint = payload.summary?.checkpoint || {};
4956
+ const recap = payload.summary?.recap || {};
4957
+ console.log(
4958
+ pc.green(
4959
+ `senti tick: relayed=${Number(payload.summary?.humanMessages?.relayed || 0)} recap=${recap.emitted ? recap.mode || "emitted" : recap.reason || "none"} checkpoint=${checkpoint.created ? checkpoint.checkpointId || "created" : checkpoint.reason || "none"}`,
4960
+ ),
4961
+ );
4896
4962
  return;
4963
+ } finally {
4964
+ if (restoreConsoleLog) {
4965
+ restoreConsoleLog();
4966
+ }
4897
4967
  }
4898
- const checkpoint = payload.summary?.checkpoint || {};
4899
- const recap = payload.summary?.recap || {};
4900
- console.log(
4901
- pc.green(
4902
- `senti tick: relayed=${Number(payload.summary?.humanMessages?.relayed || 0)} recap=${recap.emitted ? recap.mode || "emitted" : recap.reason || "none"} checkpoint=${checkpoint.created ? checkpoint.checkpointId || "created" : checkpoint.reason || "none"}`,
4903
- ),
4904
- );
4905
- return;
4906
4968
  }
4907
4969
 
4908
4970
  const controller = new AbortController();
@@ -4992,6 +5054,14 @@ export function registerSessionCommand(program) {
4992
5054
  } else {
4993
5055
  console.log(pc.gray(`senti daemon stopped (${stopped?.reason || "signal"}).`));
4994
5056
  }
5057
+ if (restoreConsoleLog) {
5058
+ restoreConsoleLog();
5059
+ }
5060
+ }
5061
+ } finally {
5062
+ if (restoreConsoleLog) {
5063
+ restoreConsoleLog();
5064
+ }
4995
5065
  }
4996
5066
  });
4997
5067