aisnitch 0.2.23 → 0.2.25

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.
@@ -771,7 +771,7 @@ var import_commander = require("commander");
771
771
 
772
772
  // src/package-info.ts
773
773
  var AISNITCH_PACKAGE_NAME = "aisnitch";
774
- var AISNITCH_VERSION = "0.2.23";
774
+ var AISNITCH_VERSION = "0.2.25";
775
775
  var AISNITCH_DESCRIPTION = "Universal bridge for AI coding tool activity \u2014 capture, normalize, stream.";
776
776
 
777
777
  // src/core/events/schema.ts
@@ -1955,6 +1955,9 @@ var OpenClawSetup = class {
1955
1955
  hookHandlerPath;
1956
1956
  hookUrl;
1957
1957
  openclawHomeDirectory;
1958
+ pluginDirectory;
1959
+ pluginFilePath;
1960
+ pluginDocumentPath;
1958
1961
  toolName = "openclaw";
1959
1962
  constructor(httpPort, dependencies = {}) {
1960
1963
  this.binaryExists = dependencies.binaryExists ?? isBinaryAvailable;
@@ -1968,6 +1971,13 @@ var OpenClawSetup = class {
1968
1971
  this.hookDocumentPath = (0, import_node_path2.join)(this.hookDirectory, "HOOK.md");
1969
1972
  this.hookHandlerPath = (0, import_node_path2.join)(this.hookDirectory, "handler.ts");
1970
1973
  this.hookUrl = `http://localhost:${httpPort}/hooks/openclaw`;
1974
+ this.pluginDirectory = (0, import_node_path2.join)(
1975
+ this.openclawHomeDirectory,
1976
+ "plugins",
1977
+ "aisnitch-monitor"
1978
+ );
1979
+ this.pluginFilePath = (0, import_node_path2.join)(this.pluginDirectory, "index.ts");
1980
+ this.pluginDocumentPath = (0, import_node_path2.join)(this.pluginDirectory, "README.md");
1971
1981
  }
1972
1982
  async detect() {
1973
1983
  return await this.binaryExists("openclaw") || await fileExists(this.openclawHomeDirectory) || await fileExists("/Applications/OpenClaw.app");
@@ -1979,9 +1989,13 @@ var OpenClawSetup = class {
1979
1989
  const currentConfigContent = await readOptionalFile(this.configPath);
1980
1990
  const currentHookDocument = await readOptionalFile(this.hookDocumentPath);
1981
1991
  const currentHookHandler = await readOptionalFile(this.hookHandlerPath);
1992
+ const currentPluginDocument = await readOptionalFile(this.pluginDocumentPath);
1993
+ const currentPluginFile = await readOptionalFile(this.pluginFilePath);
1982
1994
  const nextConfigContent = this.buildNextConfigContent(currentConfigContent);
1983
1995
  const nextHookDocument = buildOpenClawHookDocumentSource();
1984
1996
  const nextHookHandler = buildOpenClawHookHandlerSource(this.hookUrl);
1997
+ const nextPluginDocument = buildOpenClawPluginDocumentSource();
1998
+ const nextPluginFile = buildOpenClawPluginSource(this.hookUrl);
1985
1999
  return [
1986
2000
  renderColoredDiff(
1987
2001
  this.configPath,
@@ -1999,6 +2013,18 @@ var OpenClawSetup = class {
1999
2013
  this.hookHandlerPath,
2000
2014
  currentHookHandler,
2001
2015
  nextHookHandler
2016
+ ),
2017
+ "",
2018
+ renderColoredDiff(
2019
+ this.pluginDocumentPath,
2020
+ currentPluginDocument,
2021
+ nextPluginDocument
2022
+ ),
2023
+ "",
2024
+ renderColoredDiff(
2025
+ this.pluginFilePath,
2026
+ currentPluginFile,
2027
+ nextPluginFile
2002
2028
  )
2003
2029
  ].join("\n");
2004
2030
  }
@@ -2006,11 +2032,16 @@ var OpenClawSetup = class {
2006
2032
  const currentConfigContent = await readOptionalFile(this.configPath);
2007
2033
  const currentHookDocument = await readOptionalFile(this.hookDocumentPath);
2008
2034
  const currentHookHandler = await readOptionalFile(this.hookHandlerPath);
2035
+ const currentPluginDocument = await readOptionalFile(this.pluginDocumentPath);
2036
+ const currentPluginFile = await readOptionalFile(this.pluginFilePath);
2009
2037
  const nextConfigContent = this.buildNextConfigContent(currentConfigContent);
2010
2038
  const nextHookDocument = buildOpenClawHookDocumentSource();
2011
2039
  const nextHookHandler = buildOpenClawHookHandlerSource(this.hookUrl);
2040
+ const nextPluginDocument = buildOpenClawPluginDocumentSource();
2041
+ const nextPluginFile = buildOpenClawPluginSource(this.hookUrl);
2012
2042
  await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(this.configPath), { recursive: true });
2013
2043
  await (0, import_promises2.mkdir)(this.hookDirectory, { recursive: true });
2044
+ await (0, import_promises2.mkdir)(this.pluginDirectory, { recursive: true });
2014
2045
  if (currentConfigContent !== null) {
2015
2046
  await (0, import_promises2.copyFile)(this.configPath, this.getBackupPath(this.configPath));
2016
2047
  }
@@ -2026,14 +2057,30 @@ var OpenClawSetup = class {
2026
2057
  this.getBackupPath(this.hookHandlerPath)
2027
2058
  );
2028
2059
  }
2060
+ if (currentPluginDocument !== null) {
2061
+ await (0, import_promises2.copyFile)(
2062
+ this.pluginDocumentPath,
2063
+ this.getBackupPath(this.pluginDocumentPath)
2064
+ );
2065
+ }
2066
+ if (currentPluginFile !== null) {
2067
+ await (0, import_promises2.copyFile)(
2068
+ this.pluginFilePath,
2069
+ this.getBackupPath(this.pluginFilePath)
2070
+ );
2071
+ }
2029
2072
  await (0, import_promises2.writeFile)(this.configPath, nextConfigContent, "utf8");
2030
2073
  await (0, import_promises2.writeFile)(this.hookDocumentPath, nextHookDocument, "utf8");
2031
2074
  await (0, import_promises2.writeFile)(this.hookHandlerPath, nextHookHandler, "utf8");
2075
+ await (0, import_promises2.writeFile)(this.pluginDocumentPath, nextPluginDocument, "utf8");
2076
+ await (0, import_promises2.writeFile)(this.pluginFilePath, nextPluginFile, "utf8");
2032
2077
  }
2033
2078
  async revert() {
2034
2079
  await restoreBackupOrRemove(this.configPath);
2035
2080
  await restoreBackupOrRemove(this.hookDocumentPath);
2036
2081
  await restoreBackupOrRemove(this.hookHandlerPath);
2082
+ await restoreBackupOrRemove(this.pluginDocumentPath);
2083
+ await restoreBackupOrRemove(this.pluginFilePath);
2037
2084
  }
2038
2085
  buildNextConfigContent(currentContent) {
2039
2086
  const parsedConfig = parseOpenClawSettings(currentContent);
@@ -2055,6 +2102,15 @@ var OpenClawSetup = class {
2055
2102
  enabled: true
2056
2103
  }
2057
2104
  };
2105
+ const currentPlugins = parsedConfig.plugins ?? {};
2106
+ const currentPluginEntries = currentPlugins.entries ?? {};
2107
+ const nextPluginEntries = {
2108
+ ...currentPluginEntries,
2109
+ "aisnitch-monitor": {
2110
+ ...currentPluginEntries["aisnitch-monitor"] ?? {},
2111
+ enabled: true
2112
+ }
2113
+ };
2058
2114
  const nextConfig = {
2059
2115
  ...parsedConfig,
2060
2116
  hooks: {
@@ -2064,6 +2120,10 @@ var OpenClawSetup = class {
2064
2120
  enabled: true,
2065
2121
  entries: nextEntries
2066
2122
  }
2123
+ },
2124
+ plugins: {
2125
+ ...currentPlugins,
2126
+ entries: nextPluginEntries
2067
2127
  }
2068
2128
  };
2069
2129
  return `${JSON.stringify(nextConfig, null, 2)}
@@ -2898,6 +2958,231 @@ export default async function aisnitchForward(event) {
2898
2958
  }
2899
2959
  `;
2900
2960
  }
2961
+ function buildOpenClawPluginDocumentSource() {
2962
+ return `# AISnitch Monitor Plugin
2963
+
2964
+ \u{1F4D6} This managed plugin uses OpenClaw's Plugin SDK to forward rich real-time
2965
+ events to the local AISnitch HTTP receiver. Unlike the internal hook handler,
2966
+ the plugin has access to tool-level hooks (before_tool_call, after_tool_call),
2967
+ model-level hooks (model_call_started, model_call_ended), and agent turn hooks
2968
+ (agent_end, before_agent_run) \u2014 providing maximum observability fidelity.
2969
+
2970
+ ## Hooked Events
2971
+
2972
+ | Hook | AISnitch Event |
2973
+ |:---|:---|
2974
+ | gateway_start | session.start |
2975
+ | gateway_stop | session.end |
2976
+ | before_agent_run | task.start |
2977
+ | agent_end | task.complete |
2978
+ | before_tool_call | agent.coding / agent.tool_call |
2979
+ | after_tool_call | agent.coding / agent.tool_call + results |
2980
+ | model_call_started | agent.thinking |
2981
+ | model_call_ended | agent.streaming |
2982
+ | before_compaction | agent.compact |
2983
+ | after_compaction | agent.compact |
2984
+ | message_received | context info |
2985
+
2986
+ ## Managed by AISnitch
2987
+
2988
+ This file is managed by \`aisnitch setup openclaw\`. Re-running setup will
2989
+ overwrite it. Use \`aisnitch setup openclaw --revert\` to remove it.
2990
+ `;
2991
+ }
2992
+ function buildOpenClawPluginSource(hookUrl) {
2993
+ return `/**
2994
+ * AISnitch OpenClaw Plugin
2995
+ *
2996
+ * \u{1F4D6} Uses OpenClaw's Plugin SDK to forward rich real-time events to AISnitch.
2997
+ * Provides maximum visibility into tool calls, model usage, agent turns,
2998
+ * and lifecycle events \u2014 far beyond what passive file-watching can achieve.
2999
+ *
3000
+ * Managed by \`aisnitch setup openclaw\`. Re-running setup overwrites this file.
3001
+ */
3002
+
3003
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3004
+
3005
+ const AISNITCH_ENDPOINT = ${JSON.stringify(hookUrl)};
3006
+
3007
+ async function postToAISnitch(payload: Record<string, unknown>): Promise<void> {
3008
+ try {
3009
+ await fetch(AISNITCH_ENDPOINT, {
3010
+ method: "POST",
3011
+ headers: { "content-type": "application/json" },
3012
+ body: JSON.stringify(payload),
3013
+ });
3014
+ } catch {
3015
+ // Silently ignore transport errors so OpenClaw keeps running.
3016
+ }
3017
+ }
3018
+
3019
+ function getSessionKey(ctx: Record<string, unknown> | undefined): string | undefined {
3020
+ if (!ctx) return undefined;
3021
+ return (
3022
+ (typeof ctx.sessionKey === "string" && ctx.sessionKey.length > 0 ? ctx.sessionKey : undefined) ??
3023
+ (typeof ctx.sessionId === "string" && ctx.sessionId.length > 0 ? ctx.sessionId : undefined)
3024
+ );
3025
+ }
3026
+
3027
+ function getWorkspaceDir(ctx: Record<string, unknown> | undefined): string | undefined {
3028
+ if (!ctx) return undefined;
3029
+ return (
3030
+ (typeof ctx.workspaceDir === "string" && ctx.workspaceDir.length > 0 ? ctx.workspaceDir : undefined) ??
3031
+ (typeof ctx.cwd === "string" && ctx.cwd.length > 0 ? ctx.cwd : undefined)
3032
+ );
3033
+ }
3034
+
3035
+ export default definePluginEntry({
3036
+ id: "aisnitch-monitor",
3037
+ name: "AISnitch Monitor",
3038
+ register(api) {
3039
+ // \u2500\u2500 Gateway lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3040
+
3041
+ api.on("gateway_start", (event) => {
3042
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3043
+ void postToAISnitch({
3044
+ event: "gateway:startup",
3045
+ sessionKey: getSessionKey(ctx),
3046
+ context: ctx ?? {},
3047
+ timestamp: new Date().toISOString(),
3048
+ });
3049
+ });
3050
+
3051
+ api.on("gateway_stop", () => {
3052
+ void postToAISnitch({
3053
+ event: "gateway:shutdown",
3054
+ timestamp: new Date().toISOString(),
3055
+ });
3056
+ });
3057
+
3058
+ // \u2500\u2500 Agent turn lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3059
+
3060
+ api.on("before_agent_run", (event) => {
3061
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3062
+ void postToAISnitch({
3063
+ event: "command:new",
3064
+ sessionKey: getSessionKey(ctx),
3065
+ context: {
3066
+ ...ctx,
3067
+ message: event.prompt ?? event.message ?? (ctx as any)?.bodyForAgent,
3068
+ },
3069
+ timestamp: new Date().toISOString(),
3070
+ });
3071
+ });
3072
+
3073
+ api.on("agent_end", (event) => {
3074
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3075
+ void postToAISnitch({
3076
+ event: "command:stop",
3077
+ sessionKey: getSessionKey(ctx),
3078
+ context: ctx ?? {},
3079
+ success: event.success,
3080
+ durationMs: event.durationMs,
3081
+ timestamp: new Date().toISOString(),
3082
+ });
3083
+ });
3084
+
3085
+ // \u2500\u2500 Model calls \u2192 thinking / streaming \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3086
+
3087
+ api.on("model_call_started", (event) => {
3088
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3089
+ void postToAISnitch({
3090
+ event: "model_call_started",
3091
+ sessionKey: getSessionKey(ctx),
3092
+ cwd: getWorkspaceDir(ctx),
3093
+ context: ctx ?? {},
3094
+ model: event.model,
3095
+ provider: event.provider,
3096
+ timestamp: new Date().toISOString(),
3097
+ });
3098
+ });
3099
+
3100
+ api.on("model_call_ended", (event) => {
3101
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3102
+ void postToAISnitch({
3103
+ event: "model_call_ended",
3104
+ sessionKey: getSessionKey(ctx),
3105
+ cwd: getWorkspaceDir(ctx),
3106
+ context: ctx ?? {},
3107
+ model: event.model,
3108
+ durationMs: event.durationMs,
3109
+ outcome: event.outcome,
3110
+ timestamp: new Date().toISOString(),
3111
+ });
3112
+ });
3113
+
3114
+ // \u2500\u2500 Tool calls \u2014 richest signal for AISnitch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3115
+
3116
+ api.on("before_tool_call", (event) => {
3117
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3118
+ void postToAISnitch({
3119
+ event: "before_tool_call",
3120
+ sessionKey: getSessionKey(ctx),
3121
+ cwd: getWorkspaceDir(ctx),
3122
+ context: ctx ?? {},
3123
+ toolName: event.toolName,
3124
+ toolInput: event.params,
3125
+ timestamp: new Date().toISOString(),
3126
+ });
3127
+ });
3128
+
3129
+ api.on("after_tool_call", (event) => {
3130
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3131
+ void postToAISnitch({
3132
+ event: "tool_result_persist",
3133
+ sessionKey: getSessionKey(ctx),
3134
+ cwd: getWorkspaceDir(ctx),
3135
+ context: ctx ?? {},
3136
+ toolName: event.toolName,
3137
+ toolInput: event.params,
3138
+ error: event.error,
3139
+ duration: event.duration,
3140
+ result: event.result,
3141
+ timestamp: new Date().toISOString(),
3142
+ });
3143
+ });
3144
+
3145
+ // \u2500\u2500 Compaction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3146
+
3147
+ api.on("before_compaction", (event) => {
3148
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3149
+ void postToAISnitch({
3150
+ event: "before_compaction",
3151
+ sessionKey: getSessionKey(ctx),
3152
+ cwd: getWorkspaceDir(ctx),
3153
+ context: ctx ?? {},
3154
+ timestamp: new Date().toISOString(),
3155
+ });
3156
+ });
3157
+
3158
+ api.on("after_compaction", (event) => {
3159
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3160
+ void postToAISnitch({
3161
+ event: "session:compact:after",
3162
+ sessionKey: getSessionKey(ctx),
3163
+ cwd: getWorkspaceDir(ctx),
3164
+ context: ctx ?? {},
3165
+ timestamp: new Date().toISOString(),
3166
+ });
3167
+ });
3168
+
3169
+ // \u2500\u2500 Messages \u2014 user input context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3170
+
3171
+ api.on("message_received", (event) => {
3172
+ const ctx = event.ctx as Record<string, unknown> | undefined;
3173
+ void postToAISnitch({
3174
+ event: "message:received",
3175
+ sessionKey: getSessionKey(ctx),
3176
+ cwd: getWorkspaceDir(ctx),
3177
+ context: ctx ?? {},
3178
+ message: event.message,
3179
+ timestamp: new Date().toISOString(),
3180
+ });
3181
+ });
3182
+ },
3183
+ });
3184
+ `;
3185
+ }
2901
3186
  function parseOpenClawSettings(currentContent) {
2902
3187
  if (currentContent === null || currentContent.trim().length === 0) {
2903
3188
  return {};
@@ -10751,6 +11036,7 @@ var OpenClawAdapter = class extends BaseAdapter {
10751
11036
  displayName = "OpenClaw";
10752
11037
  name = "openclaw";
10753
11038
  strategies = [
11039
+ "plugin",
10754
11040
  "hooks",
10755
11041
  "log-watch",
10756
11042
  "jsonl-watch",
@@ -10970,11 +11256,32 @@ var OpenClawAdapter = class extends BaseAdapter {
10970
11256
  await this.emitOpenClawSessionEnd(sharedData, context);
10971
11257
  return;
10972
11258
  }
10973
- case "session:compact:before":
10974
- case "before_compaction": {
11259
+ case "model_call_started": {
10975
11260
  await this.ensureSessionStarted(sharedData, context);
10976
11261
  this.clearThinking(sessionId);
10977
- await this.emitStateChange("agent.compact", sharedData, context);
11262
+ await this.emitStateChange("agent.thinking", sharedData, context);
11263
+ return;
11264
+ }
11265
+ case "model_call_ended": {
11266
+ await this.ensureSessionStarted(sharedData, context);
11267
+ await this.emitStateChange("agent.streaming", {
11268
+ ...sharedData,
11269
+ raw: {
11270
+ ...sharedData.raw ?? {},
11271
+ durationMs: getNumber7(payload, "durationMs"),
11272
+ outcome: getString9(payload, "outcome"),
11273
+ source: "plugin"
11274
+ }
11275
+ }, context);
11276
+ return;
11277
+ }
11278
+ case "before_tool_call": {
11279
+ await this.ensureSessionStarted(sharedData, context);
11280
+ await this.emitStateChange(
11281
+ isOpenClawCodingTool(sharedData.toolName, sharedData.toolInput) ? "agent.coding" : "agent.tool_call",
11282
+ sharedData,
11283
+ context
11284
+ );
10978
11285
  return;
10979
11286
  }
10980
11287
  case "tool_result_persist": {
@@ -10987,6 +11294,13 @@ var OpenClawAdapter = class extends BaseAdapter {
10987
11294
  this.scheduleThinking(sessionId, sharedData, context, POST_TOOL_THINKING_DELAY_MS);
10988
11295
  return;
10989
11296
  }
11297
+ case "session:compact:before":
11298
+ case "before_compaction": {
11299
+ await this.ensureSessionStarted(sharedData, context);
11300
+ this.clearThinking(sessionId);
11301
+ await this.emitStateChange("agent.compact", sharedData, context);
11302
+ return;
11303
+ }
10990
11304
  case "message:received":
10991
11305
  case "message:preprocessed":
10992
11306
  case "session:compact:after":
@@ -11446,6 +11760,7 @@ function buildOpenClawEventData(payload) {
11446
11760
  return {
11447
11761
  activeFile: extractOpenClawActiveFile(payload) ?? toolInput?.filePath,
11448
11762
  cwd,
11763
+ duration: getNumber7(payload, "duration") ?? getNumber7(payload, "durationMs"),
11449
11764
  errorMessage: extractOpenClawErrorMessage(payload),
11450
11765
  errorType: inferOpenClawErrorType(payload),
11451
11766
  model: extractOpenClawModel(payload),
@@ -11510,7 +11825,7 @@ function extractOpenClawToolInput(payload) {
11510
11825
  };
11511
11826
  }
11512
11827
  function extractOpenClawErrorMessage(payload) {
11513
- return getString9(payload, "error") ?? getString9(payload, "message") ?? getString9(getRecord8(payload.error), "message") ?? getString9(getRecord8(payload.result), "error");
11828
+ return getString9(payload, "error") ?? getString9(payload, "errorMessage") ?? getString9(payload, "message") ?? getString9(getRecord8(payload.error), "message") ?? getString9(getRecord8(payload.result), "error") ?? getString9(getRecord8(payload.result), "message");
11514
11829
  }
11515
11830
  function inferOpenClawErrorType(payload) {
11516
11831
  const errorMessage = extractOpenClawErrorMessage(payload);
@@ -15631,6 +15946,54 @@ var DAEMON_READY_POLL_INTERVAL_MS = 100;
15631
15946
  var DAEMON_STOP_TIMEOUT_MS = 4e3;
15632
15947
  var DAEMON_LOG_MAX_BYTES = 5 * 1024 * 1024;
15633
15948
  var LAUNCH_AGENT_LABEL = "com.aisnitch.daemon";
15949
+ async function resolveNodeExecutable() {
15950
+ try {
15951
+ await (0, import_promises22.access)(process.execPath, import_node_fs7.constants.X_OK);
15952
+ return process.execPath;
15953
+ } catch {
15954
+ return "node";
15955
+ }
15956
+ }
15957
+ function formatSpawnError(error) {
15958
+ if (error instanceof Error) {
15959
+ return error.message;
15960
+ }
15961
+ return String(error);
15962
+ }
15963
+ async function pathExists(path2) {
15964
+ try {
15965
+ await (0, import_promises22.access)(path2, import_node_fs7.constants.R_OK);
15966
+ return true;
15967
+ } catch {
15968
+ return false;
15969
+ }
15970
+ }
15971
+ async function resolveDashboardDistPath() {
15972
+ if (process.env.AISNITCH_DASHBOARD_DIST) {
15973
+ const overridePath = process.env.AISNITCH_DASHBOARD_DIST;
15974
+ if (await pathExists((0, import_node_path22.join)(overridePath, "index.html"))) {
15975
+ return overridePath;
15976
+ }
15977
+ throw new Error(
15978
+ `Fullscreen dashboard assets are missing at ${overridePath}. Reinstall AISnitch or run \`pnpm --filter aisnitch-fullscreen-dashboard build\` from the repository checkout.`
15979
+ );
15980
+ }
15981
+ const cliEntryPath = process.argv[1] ? await (0, import_promises22.realpath)(process.argv[1]).catch(() => process.argv[1] ?? "") : "";
15982
+ const moduleDirectory = (0, import_node_path22.dirname)(cliEntryPath);
15983
+ const packageRoot = (0, import_node_path22.dirname)((0, import_node_path22.dirname)(moduleDirectory));
15984
+ const candidates = [
15985
+ (0, import_node_path22.join)(packageRoot, "examples", "fullscreen-dashboard", "dist"),
15986
+ (0, import_node_path22.join)(process.cwd(), "examples", "fullscreen-dashboard", "dist")
15987
+ ];
15988
+ for (const candidate of candidates) {
15989
+ if (await pathExists((0, import_node_path22.join)(candidate, "index.html"))) {
15990
+ return candidate;
15991
+ }
15992
+ }
15993
+ throw new Error(
15994
+ "Fullscreen dashboard assets are missing. Reinstall AISnitch or run `pnpm --filter aisnitch-fullscreen-dashboard build` from the repository checkout."
15995
+ );
15996
+ }
15634
15997
  function createCliRuntime(dependencies = {}) {
15635
15998
  const output = dependencies.output ?? createProcessOutput();
15636
15999
  const fetchImplementation = dependencies.fetch ?? globalThis.fetch;
@@ -16130,11 +16493,11 @@ function createCliRuntime(dependencies = {}) {
16130
16493
  }
16131
16494
  async function fullscreen(options) {
16132
16495
  const snapshot = await getStatusSnapshot(options);
16133
- if (!snapshot.running && options.daemonMode) {
16496
+ if (!snapshot.running && options.daemon) {
16134
16497
  output.stdout("Starting daemon...\n");
16135
16498
  await startDetachedDaemon(options);
16136
16499
  }
16137
- if (!snapshot.running && !options.daemonMode) {
16500
+ if (!snapshot.running && !options.daemon) {
16138
16501
  throw new Error(
16139
16502
  "AISnitch daemon is not running. Start one with `aisnitch start --daemon` or use `aisnitch fs --daemon` to start and open the dashboard."
16140
16503
  );
@@ -16146,33 +16509,77 @@ function createCliRuntime(dependencies = {}) {
16146
16509
  }
16147
16510
  const dashboardPort = options.dashboardPort ?? 5174;
16148
16511
  const dashboardUrl = `http://127.0.0.1:${dashboardPort}`;
16149
- const distPath = (0, import_node_path22.join)(process.cwd(), "examples", "fullscreen-dashboard", "dist");
16512
+ const distPath = await resolveDashboardDistPath();
16150
16513
  output.stdout(`Starting dashboard server on port ${dashboardPort}...
16151
16514
  `);
16152
- const viteProcess = spawnImplementation(process.execPath, [
16515
+ const nodeExecutable = await resolveNodeExecutable();
16516
+ const viteProcess = spawnImplementation(nodeExecutable, [
16153
16517
  "-e",
16154
16518
  `
16155
- import { createServer } from 'vite';
16156
- import path from 'path';
16519
+ import { createReadStream } from 'node:fs';
16520
+ import { stat } from 'node:fs/promises';
16521
+ import { createServer } from 'node:http';
16522
+ import { extname, join, normalize, resolve } from 'node:path';
16157
16523
 
16158
- const distPath = '${distPath}';
16524
+ const distPath = ${JSON.stringify(distPath)};
16159
16525
  const port = ${dashboardPort};
16526
+ const root = resolve(distPath);
16527
+ const contentTypes = new Map([
16528
+ ['.css', 'text/css; charset=utf-8'],
16529
+ ['.html', 'text/html; charset=utf-8'],
16530
+ ['.js', 'text/javascript; charset=utf-8'],
16531
+ ['.json', 'application/json; charset=utf-8'],
16532
+ ['.map', 'application/json; charset=utf-8'],
16533
+ ['.svg', 'image/svg+xml'],
16534
+ ]);
16160
16535
 
16161
- const server = await createServer({
16162
- root: distPath,
16163
- server: {
16164
- port,
16165
- strictPort: true,
16166
- allowedHosts: true,
16167
- },
16168
- preview: {
16169
- port,
16170
- strictPort: true,
16171
- },
16536
+ function safePath(url) {
16537
+ const parsed = new URL(url ?? '/', 'http://127.0.0.1');
16538
+ const pathname = parsed.pathname === '/' ? '/index.html' : parsed.pathname;
16539
+ const decoded = decodeURIComponent(pathname);
16540
+ const normalized = normalize(decoded).replace(/^[/\\]+/, '');
16541
+ const absolute = resolve(join(root, normalized));
16542
+
16543
+ if (absolute !== root && !absolute.startsWith(root + '/')) {
16544
+ return null;
16545
+ }
16546
+
16547
+ return absolute;
16548
+ }
16549
+
16550
+ await stat(join(root, 'index.html'));
16551
+
16552
+ const server = createServer(async (request, response) => {
16553
+ const requestedPath = safePath(request.url);
16554
+
16555
+ if (requestedPath === null) {
16556
+ response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
16557
+ response.end('Forbidden');
16558
+ return;
16559
+ }
16560
+
16561
+ try {
16562
+ const fileStat = await stat(requestedPath);
16563
+ const filePath = fileStat.isFile() ? requestedPath : join(root, 'index.html');
16564
+ const contentType = contentTypes.get(extname(filePath)) ?? 'application/octet-stream';
16565
+
16566
+ response.writeHead(200, { 'content-type': contentType });
16567
+ createReadStream(filePath).pipe(response);
16568
+ } catch {
16569
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
16570
+ createReadStream(join(root, 'index.html')).pipe(response);
16571
+ }
16172
16572
  });
16173
16573
 
16174
- await server.listen();
16175
- console.log('READY');
16574
+ server.on('error', (error) => {
16575
+ console.error(error instanceof Error ? error.message : String(error));
16576
+ process.exitCode = 1;
16577
+ });
16578
+
16579
+ await new Promise((resolveListen, rejectListen) => {
16580
+ server.once('error', rejectListen);
16581
+ server.listen(port, '127.0.0.1', resolveListen);
16582
+ });
16176
16583
 
16177
16584
  process.stdin.resume();
16178
16585
  `
@@ -16181,6 +16588,12 @@ process.stdin.resume();
16181
16588
  stdio: ["pipe", "pipe", "pipe"]
16182
16589
  });
16183
16590
  let serverOutput = "";
16591
+ let serverSpawnError;
16592
+ viteProcess.on("error", (error) => {
16593
+ serverSpawnError = error;
16594
+ serverOutput += `Dashboard server process failed: ${formatSpawnError(error)}
16595
+ `;
16596
+ });
16184
16597
  viteProcess.stdout?.on("data", (data) => {
16185
16598
  serverOutput += data.toString();
16186
16599
  });
@@ -16198,11 +16611,19 @@ process.stdin.resume();
16198
16611
  }
16199
16612
  } catch {
16200
16613
  }
16614
+ if (serverSpawnError !== void 0) {
16615
+ break;
16616
+ }
16201
16617
  if (!viteProcess.pid) break;
16202
16618
  }
16203
16619
  if (!serverReady) {
16204
16620
  output.stdout(`Server output: ${serverOutput}
16205
16621
  `);
16622
+ if (serverSpawnError !== void 0) {
16623
+ throw new Error(
16624
+ `Failed to start dashboard server process with ${nodeExecutable}: ${formatSpawnError(serverSpawnError)}`
16625
+ );
16626
+ }
16206
16627
  throw new Error("Failed to start dashboard server");
16207
16628
  }
16208
16629
  output.stdout(`Dashboard ready at ${dashboardUrl}