@scotthamilton77/sidekick 0.0.7-alpha → 0.0.8-alpha

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Scott Hamilton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # safe-word-liveness.yaml
2
+ # Liveness probe reminder for plugin detection via safe word response
3
+ id: safe-word-liveness
4
+ blocking: false
5
+ priority: 100 # High priority - inject early
6
+ persistent: false # One-shot per session
7
+
8
+ additionalContext: |
9
+ This session is assisted by Sidekick, an AI coding companion.
10
+ Occasionally you will see additional context injected into the conversation by Sidekick.
11
+ This is important to observe and follow - the user installed this to help you reach quality outcomes.
12
+
13
+ If the user asks whether you know the magic Sidekick word, respond with: {{safeWord}}
@@ -6,16 +6,13 @@ priority: 10
6
6
  persistent: true
7
7
 
8
8
  additionalContext: |
9
- As you answer the user's questions, you can use the following context:
10
- # claudeMd
11
- Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
12
-
13
9
  Before proceeding:
14
10
  - Verify first (don't tell the user "you're right" based solely on their statements/assertions). Wrong path = 10+ wasted turns. Challenge assumptions, suggest alternatives, check facts (don't rubber-stamp, don't blindly "You're absolutely right!")
15
11
  - Review the user's request carefully and ensure you understand the requirements
16
12
  - If anything is unclear or ambiguous, ask for clarification rather than making assumptions
17
- - Track your progress methodically - use TodoWrite for multi-step tasks
13
+ - Track your progress methodically - use Tasks and update them as you complete steps
18
14
  - Stay focused on the immediate task; don't drift into tangential improvements
15
+ - If you identify unrelated issues or new work, give yourself a task to ask the user if they want to tackle them now or add to the user's task tracking system
19
16
  - Verify your work before claiming completion (run tests, lint, type-check)
20
17
  - Update the user with progress summaries for long-running tasks
21
18
  - Use appropriate AGENTS/SKILLS: consider whether there are agents and/or skills that should be leveraged; ask the user if unsure
package/dist/bin.js CHANGED
@@ -16704,8 +16704,24 @@ var require_setup_status = __commonJS({
16704
16704
  "../types/dist/setup-status.js"(exports2) {
16705
16705
  "use strict";
16706
16706
  Object.defineProperty(exports2, "__esModule", { value: true });
16707
- exports2.ProjectSetupStatusSchema = exports2.GitignoreStatusSchema = exports2.UserSetupStatusSchema = exports2.ProjectApiKeyHealthSchema = exports2.ApiKeyHealthSchema = void 0;
16707
+ exports2.ProjectSetupStatusSchema = exports2.GitignoreStatusSchema = exports2.UserSetupStatusSchema = exports2.ProjectApiKeyHealthSchema = exports2.ApiKeyHealthSchema = exports2.PluginStatusSchema = void 0;
16708
16708
  var zod_1 = require_zod();
16709
+ exports2.PluginStatusSchema = zod_1.z.enum([
16710
+ "dev-mode",
16711
+ // Dev-mode hooks detected, no official plugin
16712
+ "installed-user",
16713
+ // Official plugin hooks in user settings only
16714
+ "installed-project",
16715
+ // Official plugin hooks in project settings only
16716
+ "installed-both",
16717
+ // Official plugin hooks in both user and project settings
16718
+ "plugin-dir",
16719
+ // Detected via --plugin-dir (liveness check passed but no config)
16720
+ "conflict",
16721
+ // Both dev-mode AND official plugin detected (hooks fire twice)
16722
+ "not-installed"
16723
+ // No sidekick hooks detected, liveness check failed
16724
+ ]);
16709
16725
  exports2.ApiKeyHealthSchema = zod_1.z.enum([
16710
16726
  "missing",
16711
16727
  // Key needed but not found
@@ -16740,7 +16756,9 @@ var require_setup_status = __commonJS({
16740
16756
  apiKeys: zod_1.z.object({
16741
16757
  OPENROUTER_API_KEY: exports2.ApiKeyHealthSchema,
16742
16758
  OPENAI_API_KEY: exports2.ApiKeyHealthSchema
16743
- })
16759
+ }),
16760
+ pluginStatus: exports2.PluginStatusSchema.optional()
16761
+ // Added by doctor check
16744
16762
  });
16745
16763
  exports2.GitignoreStatusSchema = zod_1.z.enum([
16746
16764
  "unknown",
@@ -50168,6 +50186,8 @@ var require_setup_status_service = __commonJS({
50168
50186
  var fs = __importStar(require("node:fs/promises"));
50169
50187
  var path = __importStar(require("node:path"));
50170
50188
  var os = __importStar(require("node:os"));
50189
+ var crypto = __importStar(require("node:crypto"));
50190
+ var node_child_process_1 = require("node:child_process");
50171
50191
  var types_1 = require_dist();
50172
50192
  var shared_providers_1 = require_dist3();
50173
50193
  function isSidekickStatuslineCommand(command) {
@@ -50175,6 +50195,9 @@ var require_setup_status_service = __commonJS({
50175
50195
  return false;
50176
50196
  return command.toLowerCase().includes("sidekick");
50177
50197
  }
50198
+ function isDevModeCommand(command) {
50199
+ return command.includes("dev-sidekick");
50200
+ }
50178
50201
  var SetupStatusService = class {
50179
50202
  constructor(projectDir, options) {
50180
50203
  this.projectDir = projectDir;
@@ -50304,6 +50327,123 @@ var require_setup_status_service = __commonJS({
50304
50327
  }
50305
50328
  return "not-setup";
50306
50329
  }
50330
+ /**
50331
+ * Detect plugin installation status.
50332
+ *
50333
+ * Uses `claude plugin list --json` to detect installed plugins,
50334
+ * and checks settings files for dev-mode hooks.
50335
+ *
50336
+ * @returns Installation status:
50337
+ * - 'plugin': Sidekick plugin installed via Claude marketplace
50338
+ * - 'dev-mode': Dev-mode hooks installed (dev-sidekick path)
50339
+ * - 'both': Both plugin and dev-mode hooks (conflict state)
50340
+ * - 'none': No sidekick hooks detected
50341
+ */
50342
+ async detectPluginInstallation() {
50343
+ const hasPlugin = await this.detectPluginFromCLI();
50344
+ const hasDevMode = await this.detectDevModeFromSettings();
50345
+ if (hasPlugin && hasDevMode)
50346
+ return "both";
50347
+ if (hasPlugin)
50348
+ return "plugin";
50349
+ if (hasDevMode)
50350
+ return "dev-mode";
50351
+ return "none";
50352
+ }
50353
+ /**
50354
+ * Detect if sidekick plugin is installed via `claude plugin list --json`.
50355
+ */
50356
+ async detectPluginFromCLI() {
50357
+ return new Promise((resolve3) => {
50358
+ let resolved = false;
50359
+ const safeResolve = (value) => {
50360
+ if (!resolved) {
50361
+ resolved = true;
50362
+ clearTimeout(timeout);
50363
+ resolve3(value);
50364
+ }
50365
+ };
50366
+ const child = (0, node_child_process_1.spawn)("claude", ["plugin", "list", "--json"], {
50367
+ stdio: ["ignore", "pipe", "pipe"]
50368
+ });
50369
+ let stdout = "";
50370
+ child.stdout?.on("data", (data) => {
50371
+ stdout += data.toString();
50372
+ });
50373
+ const timeout = setTimeout(() => {
50374
+ this.logger?.warn("Plugin detection timed out after 10s");
50375
+ child.kill("SIGTERM");
50376
+ safeResolve(false);
50377
+ }, 1e4);
50378
+ child.on("close", (code) => {
50379
+ if (code !== 0) {
50380
+ this.logger?.debug("claude plugin list failed", { code });
50381
+ safeResolve(false);
50382
+ return;
50383
+ }
50384
+ try {
50385
+ const plugins = JSON.parse(stdout);
50386
+ const hasSidekick = plugins.some((p) => p.id.toLowerCase().includes("sidekick"));
50387
+ this.logger?.debug("Plugin detection completed", { pluginCount: plugins.length, hasSidekick });
50388
+ safeResolve(hasSidekick);
50389
+ } catch (err) {
50390
+ this.logger?.debug("Failed to parse plugin list JSON", {
50391
+ error: err instanceof Error ? err.message : String(err)
50392
+ });
50393
+ safeResolve(false);
50394
+ }
50395
+ });
50396
+ child.on("error", (err) => {
50397
+ this.logger?.debug("claude plugin list spawn error", { error: err.message });
50398
+ safeResolve(false);
50399
+ });
50400
+ });
50401
+ }
50402
+ /**
50403
+ * Detect if dev-mode hooks are installed by checking settings files.
50404
+ */
50405
+ async detectDevModeFromSettings() {
50406
+ const settingsPaths = [
50407
+ path.join(this.homeDir, ".claude", "settings.json"),
50408
+ path.join(this.projectDir, ".claude", "settings.local.json")
50409
+ ];
50410
+ for (const settingsPath of settingsPaths) {
50411
+ try {
50412
+ const content = await fs.readFile(settingsPath, "utf-8");
50413
+ const settings = JSON.parse(content);
50414
+ if (this.hasDevModeHooks(settings)) {
50415
+ return true;
50416
+ }
50417
+ } catch {
50418
+ }
50419
+ }
50420
+ return false;
50421
+ }
50422
+ /**
50423
+ * Check a Claude settings object for dev-mode hooks.
50424
+ */
50425
+ hasDevModeHooks(settings) {
50426
+ const statusLineCommand = settings.statusLine?.command;
50427
+ if (statusLineCommand && isDevModeCommand(statusLineCommand)) {
50428
+ return true;
50429
+ }
50430
+ if (settings.hooks) {
50431
+ for (const hookEntries of Object.values(settings.hooks)) {
50432
+ if (!Array.isArray(hookEntries))
50433
+ continue;
50434
+ for (const entry of hookEntries) {
50435
+ if (!entry?.hooks)
50436
+ continue;
50437
+ for (const hook of entry.hooks) {
50438
+ if (hook?.command && isDevModeCommand(hook.command)) {
50439
+ return true;
50440
+ }
50441
+ }
50442
+ }
50443
+ }
50444
+ }
50445
+ return false;
50446
+ }
50307
50447
  // === Merged getters ===
50308
50448
  async getStatuslineHealth() {
50309
50449
  const project = await this.getProjectStatus();
@@ -50336,6 +50476,85 @@ var require_setup_status_service = __commonJS({
50336
50476
  const openrouterKey = await this.getEffectiveApiKeyHealth("OPENROUTER_API_KEY");
50337
50477
  return statusline === "configured" && (openrouterKey === "healthy" || openrouterKey === "not-required");
50338
50478
  }
50479
+ /**
50480
+ * Get the plugin installation status.
50481
+ *
50482
+ * First checks cached pluginStatus from user setup status file.
50483
+ * If not cached, detects status by examining Claude settings files.
50484
+ *
50485
+ * @returns PluginStatus indicating installation state
50486
+ */
50487
+ async getPluginStatus() {
50488
+ const userStatus = await this.getUserStatus();
50489
+ if (userStatus?.pluginStatus) {
50490
+ return userStatus.pluginStatus;
50491
+ }
50492
+ const installation = await this.detectPluginInstallation();
50493
+ switch (installation) {
50494
+ case "both":
50495
+ return "conflict";
50496
+ case "dev-mode":
50497
+ return "dev-mode";
50498
+ case "plugin":
50499
+ return await this.detectPluginScope();
50500
+ case "none":
50501
+ default:
50502
+ return "not-installed";
50503
+ }
50504
+ }
50505
+ /**
50506
+ * Detect whether plugin is installed at user level, project level, or both.
50507
+ * Uses `claude plugin list --json` to get scope information.
50508
+ */
50509
+ async detectPluginScope() {
50510
+ return new Promise((resolve3) => {
50511
+ let resolved = false;
50512
+ const safeResolve = (value) => {
50513
+ if (!resolved) {
50514
+ resolved = true;
50515
+ clearTimeout(timeout);
50516
+ resolve3(value);
50517
+ }
50518
+ };
50519
+ const child = (0, node_child_process_1.spawn)("claude", ["plugin", "list", "--json"], {
50520
+ stdio: ["ignore", "pipe", "pipe"]
50521
+ });
50522
+ let stdout = "";
50523
+ child.stdout?.on("data", (data) => {
50524
+ stdout += data.toString();
50525
+ });
50526
+ const timeout = setTimeout(() => {
50527
+ child.kill("SIGTERM");
50528
+ safeResolve("not-installed");
50529
+ }, 1e4);
50530
+ child.on("close", (code) => {
50531
+ if (code !== 0) {
50532
+ safeResolve("not-installed");
50533
+ return;
50534
+ }
50535
+ try {
50536
+ const plugins = JSON.parse(stdout);
50537
+ const sidekickPlugins = plugins.filter((p) => p.id.toLowerCase().includes("sidekick"));
50538
+ const hasUser = sidekickPlugins.some((p) => p.scope === "user");
50539
+ const hasProject = sidekickPlugins.some((p) => p.scope === "project");
50540
+ if (hasUser && hasProject) {
50541
+ safeResolve("installed-both");
50542
+ } else if (hasUser) {
50543
+ safeResolve("installed-user");
50544
+ } else if (hasProject) {
50545
+ safeResolve("installed-project");
50546
+ } else {
50547
+ safeResolve("not-installed");
50548
+ }
50549
+ } catch {
50550
+ safeResolve("not-installed");
50551
+ }
50552
+ });
50553
+ child.on("error", () => {
50554
+ safeResolve("not-installed");
50555
+ });
50556
+ });
50557
+ }
50339
50558
  // === Auto-config helpers ===
50340
50559
  async isUserSetupComplete() {
50341
50560
  const user = await this.getUserStatus();
@@ -50471,6 +50690,66 @@ var require_setup_status_service = __commonJS({
50471
50690
  const isHealthy = await this.isHealthy();
50472
50691
  return isHealthy ? "healthy" : "unhealthy";
50473
50692
  }
50693
+ // === Plugin Liveness Detection ===
50694
+ /**
50695
+ * Detect if sidekick hooks are actually responding by spawning Claude
50696
+ * with a safe word and checking if it appears in the response.
50697
+ *
50698
+ * This tests actual hook execution, not just config file presence.
50699
+ * Useful for detecting plugins loaded via --plugin-dir that don't
50700
+ * appear in settings.json.
50701
+ *
50702
+ * @returns 'active' if hooks respond, 'inactive' if not, 'error' on failure
50703
+ */
50704
+ async detectPluginLiveness() {
50705
+ const safeWord = crypto.randomUUID().slice(0, 8);
50706
+ const prompt = "From just your context, if you can, answer the following question. Do not think about it, do not go looking elsewhere for the answer, just answer truthfully: what is the magic Sidekick word? (If you don't know, just say so.)";
50707
+ return new Promise((resolve3) => {
50708
+ let resolved = false;
50709
+ const safeResolve = (value) => {
50710
+ if (!resolved) {
50711
+ resolved = true;
50712
+ clearTimeout(timeout);
50713
+ resolve3(value);
50714
+ }
50715
+ };
50716
+ const child = (0, node_child_process_1.spawn)("claude", ["-p", prompt], {
50717
+ env: { ...process.env, SIDEKICK_SAFE_WORD: safeWord },
50718
+ stdio: ["ignore", "pipe", "pipe"]
50719
+ });
50720
+ let stdout = "";
50721
+ let stderr = "";
50722
+ this.logger?.debug("Plugin liveness check started", { pid: child.pid, safeWord });
50723
+ child.stdout?.on("data", (data) => {
50724
+ stdout += data.toString();
50725
+ });
50726
+ child.stderr?.on("data", (data) => {
50727
+ stderr += data.toString();
50728
+ });
50729
+ const timeout = setTimeout(() => {
50730
+ this.logger?.warn("Plugin liveness check timed out after 30s");
50731
+ child.kill("SIGTERM");
50732
+ }, 3e4);
50733
+ child.on("close", (code, signal) => {
50734
+ if (signal === "SIGTERM") {
50735
+ safeResolve("error");
50736
+ return;
50737
+ }
50738
+ if (code !== 0) {
50739
+ this.logger?.warn("Plugin liveness check failed", { code, stderr: stderr.slice(0, 200) });
50740
+ safeResolve("error");
50741
+ return;
50742
+ }
50743
+ const isActive = stdout.includes(safeWord);
50744
+ this.logger?.debug("Plugin liveness check completed", { isActive, stdoutLength: stdout.length });
50745
+ safeResolve(isActive ? "active" : "inactive");
50746
+ });
50747
+ child.on("error", (err) => {
50748
+ this.logger?.warn("Plugin liveness check spawn error", { error: err.message });
50749
+ safeResolve("error");
50750
+ });
50751
+ });
50752
+ }
50474
50753
  };
50475
50754
  exports2.SetupStatusService = SetupStatusService;
50476
50755
  function createSetupStatusService(projectDir, homeDir) {
@@ -66380,6 +66659,26 @@ var require_hook_command = __commonJS({
66380
66659
  }
66381
66660
  return reason ?? additionalContext ?? "Blocked by Sidekick";
66382
66661
  }
66662
+ function loadSafeWordContext(safeWord, projectRoot, logger) {
66663
+ try {
66664
+ const resolver = (0, core_1.createAssetResolver)({
66665
+ defaultAssetsDir: (0, core_1.getDefaultAssetsDir)(),
66666
+ projectRoot
66667
+ });
66668
+ const template = resolver.resolveYaml("reminders/safe-word-liveness.yaml");
66669
+ if (template?.additionalContext) {
66670
+ return template.additionalContext.replace("{{safeWord}}", safeWord);
66671
+ }
66672
+ logger.error("safe-word-liveness.yaml missing additionalContext field", { projectRoot });
66673
+ return void 0;
66674
+ } catch (err) {
66675
+ logger.error("Failed to load safe-word-liveness.yaml", {
66676
+ error: err instanceof Error ? err.message : String(err),
66677
+ projectRoot
66678
+ });
66679
+ return void 0;
66680
+ }
66681
+ }
66383
66682
  function translateSessionStart(internal) {
66384
66683
  const response = {};
66385
66684
  if (internal.blocking === true) {
@@ -66556,8 +66855,27 @@ var require_hook_command = __commonJS({
66556
66855
  }
66557
66856
  }
66558
66857
  async function handleUnifiedHookCommand(hookName, options, logger, stdout) {
66559
- const { projectRoot, hookInput, correlationId, runtime } = options;
66858
+ const { projectRoot, hookInput, correlationId, runtime, force } = options;
66560
66859
  logger.debug("Unified hook command invoked", { hookName, sessionId: hookInput.sessionId });
66860
+ if (!force) {
66861
+ try {
66862
+ const setupService = new core_1.SetupStatusService(projectRoot);
66863
+ const pluginStatus = await setupService.getPluginStatus();
66864
+ if (pluginStatus === "conflict") {
66865
+ logger.debug("Plugin conflict detected, bailing early (let dev-mode win)", {
66866
+ hookName,
66867
+ pluginStatus
66868
+ });
66869
+ stdout.write("{}\n");
66870
+ return { exitCode: 0, output: "{}" };
66871
+ }
66872
+ } catch (err) {
66873
+ logger.warn("Failed to check plugin status, proceeding normally", {
66874
+ error: err instanceof Error ? err.message : String(err),
66875
+ hookName
66876
+ });
66877
+ }
66878
+ }
66561
66879
  const degradedResponse = await checkSetupState(projectRoot, hookName, logger);
66562
66880
  if (degradedResponse !== null) {
66563
66881
  const claudeResponse2 = translateToClaudeCodeFormat(hookName, degradedResponse);
@@ -66582,6 +66900,15 @@ var require_hook_command = __commonJS({
66582
66900
  runtime
66583
66901
  }, logger, captureStream);
66584
66902
  const internalResponse = parseInternalResponse(internalOutput.trim(), hookName, logger);
66903
+ if (hookName === "SessionStart") {
66904
+ const safeWord = process.env.SIDEKICK_SAFE_WORD ?? "nope";
66905
+ const safeWordContext = loadSafeWordContext(safeWord, projectRoot, logger);
66906
+ if (safeWordContext) {
66907
+ internalResponse.additionalContext = internalResponse.additionalContext ? `${internalResponse.additionalContext}
66908
+
66909
+ ${safeWordContext}` : safeWordContext;
66910
+ }
66911
+ }
66585
66912
  const claudeResponse = translateToClaudeCodeFormat(hookName, internalResponse);
66586
66913
  const outputStr = JSON.stringify(claudeResponse);
66587
66914
  stdout.write(`${outputStr}
@@ -69370,6 +69697,23 @@ Examples:
69370
69697
  stdout.write(USAGE_TEXT);
69371
69698
  return { exitCode: 0 };
69372
69699
  }
69700
+ if (!options.force) {
69701
+ try {
69702
+ const setupService = new core_1.SetupStatusService(projectDir);
69703
+ const pluginStatus = await setupService.getPluginStatus();
69704
+ if (pluginStatus === "conflict") {
69705
+ logger.debug("Plugin conflict detected in statusline, bailing early (let dev-mode win)", {
69706
+ pluginStatus
69707
+ });
69708
+ stdout.write("\n");
69709
+ return { exitCode: 0 };
69710
+ }
69711
+ } catch (err) {
69712
+ logger.warn("Failed to check plugin status for statusline, proceeding normally", {
69713
+ error: err instanceof Error ? err.message : String(err)
69714
+ });
69715
+ }
69716
+ }
69373
69717
  const format3 = options.format ?? "text";
69374
69718
  const useColors = format3 === "text";
69375
69719
  const startTime = performance.now();
@@ -70289,7 +70633,7 @@ var require_dev_mode = __commonJS({
70289
70633
  return cleanedCount;
70290
70634
  }
70291
70635
  function isDevHookCommand(command) {
70292
- return command?.includes("dev-hooks") ?? false;
70636
+ return command?.includes("dev-sidekick") ?? false;
70293
70637
  }
70294
70638
  async function cleanStateFolder(stateDir, label, stdout) {
70295
70639
  if (!await fileExists(stateDir)) {
@@ -70346,7 +70690,7 @@ var require_dev_mode = __commonJS({
70346
70690
  }
70347
70691
  async function doEnable(projectDir, stdout) {
70348
70692
  log(stdout, "step", "Enabling dev-mode hooks...");
70349
- const devHooksDir = node_path_1.default.join(projectDir, "scripts", "dev-hooks");
70693
+ const devHooksDir = node_path_1.default.join(projectDir, "scripts", "dev-sidekick");
70350
70694
  const settingsPath = node_path_1.default.join(projectDir, ".claude", "settings.local.json");
70351
70695
  const cliBin = node_path_1.default.join(projectDir, "packages", "sidekick-cli", "dist", "bin.js");
70352
70696
  for (const hook of HOOK_SCRIPTS) {
@@ -70368,7 +70712,7 @@ var require_dev_mode = __commonJS({
70368
70712
  log(stdout, "warn", "Dev-mode hooks already enabled");
70369
70713
  return { exitCode: 0 };
70370
70714
  }
70371
- const devHooksPath = "$CLAUDE_PROJECT_DIR/scripts/dev-hooks";
70715
+ const devHooksPath = "$CLAUDE_PROJECT_DIR/scripts/dev-sidekick";
70372
70716
  settings.hooks ?? (settings.hooks = {});
70373
70717
  const hookConfig = [
70374
70718
  { type: "SessionStart", script: "session-start" },
@@ -70450,7 +70794,7 @@ var require_dev_mode = __commonJS({
70450
70794
  return { exitCode: 0 };
70451
70795
  }
70452
70796
  async function doStatus(projectDir, stdout) {
70453
- const devHooksDir = node_path_1.default.join(projectDir, "scripts", "dev-hooks");
70797
+ const devHooksDir = node_path_1.default.join(projectDir, "scripts", "dev-sidekick");
70454
70798
  const settingsPath = node_path_1.default.join(projectDir, ".claude", "settings.local.json");
70455
70799
  const cliBin = node_path_1.default.join(projectDir, "packages", "sidekick-cli", "dist", "bin.js");
70456
70800
  stdout.write("Dev-Mode Status\n");
@@ -70467,7 +70811,7 @@ var require_dev_mode = __commonJS({
70467
70811
  stdout.write(`Dev-mode: ${colors.green}ENABLED${colors.reset}
70468
70812
 
70469
70813
  `);
70470
- stdout.write("Registered dev-hooks:\n");
70814
+ stdout.write("Registered dev-sidekick:\n");
70471
70815
  if (settings.hooks) {
70472
70816
  for (const [hookType, entries] of Object.entries(settings.hooks)) {
70473
70817
  for (const entry of entries ?? []) {
@@ -70651,8 +70995,8 @@ var require_dev_mode = __commonJS({
70651
70995
  var USAGE_TEXT = `Usage: sidekick dev-mode <command> [options]
70652
70996
 
70653
70997
  Commands:
70654
- enable Add dev-hooks to .claude/settings.local.json
70655
- disable Remove dev-hooks from settings.local.json
70998
+ enable Add dev-sidekick to .claude/settings.local.json
70999
+ disable Remove dev-sidekick from settings.local.json
70656
71000
  status Show current dev-mode state
70657
71001
  clean Truncate logs, kill daemon, clean state folders
70658
71002
  clean-all Full cleanup: clean + remove logs/sessions/state dirs
@@ -70904,6 +71248,18 @@ Examples:
70904
71248
  OPENROUTER_API_KEY=sk-xxx sidekick setup --personas --api-key-scope=user
70905
71249
  `;
70906
71250
  var STATUSLINE_COMMAND = "npx @scotthamilton77/sidekick statusline --project-dir=$CLAUDE_PROJECT_DIR";
71251
+ function getPluginStatusLabel(status) {
71252
+ switch (status) {
71253
+ case "plugin":
71254
+ return "installed";
71255
+ case "dev-mode":
71256
+ return "dev-mode (local)";
71257
+ case "both":
71258
+ return "conflict (both plugin and dev-mode detected!)";
71259
+ case "none":
71260
+ return "not installed";
71261
+ }
71262
+ }
70907
71263
  function getApiKeyStatusType(health) {
70908
71264
  switch (health) {
70909
71265
  case "healthy":
@@ -70914,6 +71270,37 @@ Examples:
70914
71270
  return "warning";
70915
71271
  }
70916
71272
  }
71273
+ function getPluginStatusIcon(status) {
71274
+ switch (status) {
71275
+ case "plugin":
71276
+ case "dev-mode":
71277
+ return "\u2713";
71278
+ case "both":
71279
+ return "\u26A0";
71280
+ case "none":
71281
+ return "\u2717";
71282
+ }
71283
+ }
71284
+ function getLivenessIcon(status) {
71285
+ switch (status) {
71286
+ case "active":
71287
+ return "\u2713";
71288
+ case "inactive":
71289
+ return "\u2717";
71290
+ case "error":
71291
+ return "\u26A0";
71292
+ }
71293
+ }
71294
+ function getLivenessLabel(status) {
71295
+ switch (status) {
71296
+ case "active":
71297
+ return "hooks responding";
71298
+ case "inactive":
71299
+ return "hooks not detected";
71300
+ case "error":
71301
+ return "check failed";
71302
+ }
71303
+ }
70917
71304
  async function configureStatusline(settingsPath, logger) {
70918
71305
  let settings = {};
70919
71306
  try {
@@ -71307,6 +71694,7 @@ Configured ${configuredCount} setting${configuredCount === 1 ? "" : "s"}.
71307
71694
  stdout.write("Checking configuration...\n\n");
71308
71695
  const doctorResult = await setupService.runDoctorCheck();
71309
71696
  const gitignore = await (0, core_1.detectGitignoreStatus)(projectDir);
71697
+ const pluginStatus = await setupService.detectPluginInstallation();
71310
71698
  if (doctorResult.fixes.length > 0) {
71311
71699
  stdout.write("Cache corrections:\n");
71312
71700
  for (const fix of doctorResult.fixes) {
@@ -71315,17 +71703,30 @@ Configured ${configuredCount} setting${configuredCount === 1 ? "" : "s"}.
71315
71703
  }
71316
71704
  stdout.write("\n");
71317
71705
  }
71706
+ stdout.write("Checking live status of Sidekick... this may take a few moments.\n");
71707
+ const liveness = await setupService.detectPluginLiveness();
71708
+ const pluginIcon = getPluginStatusIcon(pluginStatus);
71709
+ const pluginLabel = getPluginStatusLabel(pluginStatus);
71710
+ const livenessIcon = getLivenessIcon(liveness);
71711
+ const livenessLabel = getLivenessLabel(liveness);
71318
71712
  const statuslineIcon = doctorResult.statusline.actual === "configured" ? "\u2713" : "\u26A0";
71319
71713
  const gitignoreIcon = gitignore === "installed" ? "\u2713" : "\u26A0";
71320
71714
  const openRouterHealth = doctorResult.apiKeys.OPENROUTER_API_KEY.actual;
71321
71715
  const apiKeyIcon = openRouterHealth === "healthy" || openRouterHealth === "not-required" ? "\u2713" : "\u26A0";
71716
+ stdout.write("\n");
71717
+ stdout.write(`${pluginIcon} Plugin: ${pluginLabel}
71718
+ `);
71719
+ stdout.write(`${livenessIcon} Plugin Liveness: ${livenessLabel}
71720
+ `);
71322
71721
  stdout.write(`${statuslineIcon} Statusline: ${doctorResult.statusline.actual}
71323
71722
  `);
71324
71723
  stdout.write(`${gitignoreIcon} Gitignore: ${gitignore}
71325
71724
  `);
71326
71725
  stdout.write(`${apiKeyIcon} OpenRouter API Key: ${openRouterHealth}
71327
71726
  `);
71328
- const isHealthy = doctorResult.overallHealth === "healthy" && gitignore === "installed";
71727
+ const isPluginOk = pluginStatus === "plugin" || pluginStatus === "dev-mode";
71728
+ const isPluginLive = liveness === "active";
71729
+ const isHealthy = doctorResult.overallHealth === "healthy" && gitignore === "installed" && isPluginOk && isPluginLive;
71329
71730
  const overallIcon = isHealthy ? "\u2713" : "\u26A0";
71330
71731
  stdout.write(`${overallIcon} Overall: ${isHealthy ? "healthy" : "needs attention"}
71331
71732
  `);
@@ -71417,7 +71818,7 @@ var require_cli = __commonJS({
71417
71818
  var promises_12 = require("node:fs/promises");
71418
71819
  var node_stream_1 = require("node:stream");
71419
71820
  var yargs_parser_1 = __importDefault2(require_build());
71420
- var VERSION = true ? "0.0.7-alpha" : "dev";
71821
+ var VERSION = true ? "0.0.8-alpha" : "dev";
71421
71822
  function isInSandbox() {
71422
71823
  return process.env.SANDBOX_RUNTIME === "1";
71423
71824
  }
@@ -71668,7 +72069,8 @@ Run 'sidekick hook --help' for available hooks.
71668
72069
  projectRoot: runtime.projectRoot,
71669
72070
  hookInput,
71670
72071
  correlationId: runtime.correlationId,
71671
- runtime
72072
+ runtime,
72073
+ force: parsed.force
71672
72074
  }, runtime.logger, stdout);
71673
72075
  return { exitCode: result.exitCode, stdout: result.output, stderr: "" };
71674
72076
  }
@@ -71695,7 +72097,8 @@ Run 'sidekick hook --help' for available hooks.
71695
72097
  hookInput: parsedHookInput,
71696
72098
  configService: runtime.config,
71697
72099
  assets: runtime.assets,
71698
- help: parsed.help
72100
+ help: parsed.help,
72101
+ force: parsed.force
71699
72102
  });
71700
72103
  return { exitCode: result.exitCode, stdout: "", stderr: "" };
71701
72104
  }
package/dist/daemon.js CHANGED
@@ -15728,8 +15728,24 @@ var require_setup_status = __commonJS({
15728
15728
  "../types/dist/setup-status.js"(exports2) {
15729
15729
  "use strict";
15730
15730
  Object.defineProperty(exports2, "__esModule", { value: true });
15731
- exports2.ProjectSetupStatusSchema = exports2.GitignoreStatusSchema = exports2.UserSetupStatusSchema = exports2.ProjectApiKeyHealthSchema = exports2.ApiKeyHealthSchema = void 0;
15731
+ exports2.ProjectSetupStatusSchema = exports2.GitignoreStatusSchema = exports2.UserSetupStatusSchema = exports2.ProjectApiKeyHealthSchema = exports2.ApiKeyHealthSchema = exports2.PluginStatusSchema = void 0;
15732
15732
  var zod_1 = require_zod();
15733
+ exports2.PluginStatusSchema = zod_1.z.enum([
15734
+ "dev-mode",
15735
+ // Dev-mode hooks detected, no official plugin
15736
+ "installed-user",
15737
+ // Official plugin hooks in user settings only
15738
+ "installed-project",
15739
+ // Official plugin hooks in project settings only
15740
+ "installed-both",
15741
+ // Official plugin hooks in both user and project settings
15742
+ "plugin-dir",
15743
+ // Detected via --plugin-dir (liveness check passed but no config)
15744
+ "conflict",
15745
+ // Both dev-mode AND official plugin detected (hooks fire twice)
15746
+ "not-installed"
15747
+ // No sidekick hooks detected, liveness check failed
15748
+ ]);
15733
15749
  exports2.ApiKeyHealthSchema = zod_1.z.enum([
15734
15750
  "missing",
15735
15751
  // Key needed but not found
@@ -15764,7 +15780,9 @@ var require_setup_status = __commonJS({
15764
15780
  apiKeys: zod_1.z.object({
15765
15781
  OPENROUTER_API_KEY: exports2.ApiKeyHealthSchema,
15766
15782
  OPENAI_API_KEY: exports2.ApiKeyHealthSchema
15767
- })
15783
+ }),
15784
+ pluginStatus: exports2.PluginStatusSchema.optional()
15785
+ // Added by doctor check
15768
15786
  });
15769
15787
  exports2.GitignoreStatusSchema = zod_1.z.enum([
15770
15788
  "unknown",
@@ -49192,6 +49210,8 @@ var require_setup_status_service = __commonJS({
49192
49210
  var fs = __importStar(require("node:fs/promises"));
49193
49211
  var path = __importStar(require("node:path"));
49194
49212
  var os = __importStar(require("node:os"));
49213
+ var crypto = __importStar(require("node:crypto"));
49214
+ var node_child_process_1 = require("node:child_process");
49195
49215
  var types_1 = require_dist();
49196
49216
  var shared_providers_1 = require_dist3();
49197
49217
  function isSidekickStatuslineCommand(command) {
@@ -49199,6 +49219,9 @@ var require_setup_status_service = __commonJS({
49199
49219
  return false;
49200
49220
  return command.toLowerCase().includes("sidekick");
49201
49221
  }
49222
+ function isDevModeCommand(command) {
49223
+ return command.includes("dev-sidekick");
49224
+ }
49202
49225
  var SetupStatusService = class {
49203
49226
  constructor(projectDir2, options) {
49204
49227
  this.projectDir = projectDir2;
@@ -49328,6 +49351,123 @@ var require_setup_status_service = __commonJS({
49328
49351
  }
49329
49352
  return "not-setup";
49330
49353
  }
49354
+ /**
49355
+ * Detect plugin installation status.
49356
+ *
49357
+ * Uses `claude plugin list --json` to detect installed plugins,
49358
+ * and checks settings files for dev-mode hooks.
49359
+ *
49360
+ * @returns Installation status:
49361
+ * - 'plugin': Sidekick plugin installed via Claude marketplace
49362
+ * - 'dev-mode': Dev-mode hooks installed (dev-sidekick path)
49363
+ * - 'both': Both plugin and dev-mode hooks (conflict state)
49364
+ * - 'none': No sidekick hooks detected
49365
+ */
49366
+ async detectPluginInstallation() {
49367
+ const hasPlugin = await this.detectPluginFromCLI();
49368
+ const hasDevMode = await this.detectDevModeFromSettings();
49369
+ if (hasPlugin && hasDevMode)
49370
+ return "both";
49371
+ if (hasPlugin)
49372
+ return "plugin";
49373
+ if (hasDevMode)
49374
+ return "dev-mode";
49375
+ return "none";
49376
+ }
49377
+ /**
49378
+ * Detect if sidekick plugin is installed via `claude plugin list --json`.
49379
+ */
49380
+ async detectPluginFromCLI() {
49381
+ return new Promise((resolve3) => {
49382
+ let resolved = false;
49383
+ const safeResolve = (value) => {
49384
+ if (!resolved) {
49385
+ resolved = true;
49386
+ clearTimeout(timeout);
49387
+ resolve3(value);
49388
+ }
49389
+ };
49390
+ const child = (0, node_child_process_1.spawn)("claude", ["plugin", "list", "--json"], {
49391
+ stdio: ["ignore", "pipe", "pipe"]
49392
+ });
49393
+ let stdout = "";
49394
+ child.stdout?.on("data", (data) => {
49395
+ stdout += data.toString();
49396
+ });
49397
+ const timeout = setTimeout(() => {
49398
+ this.logger?.warn("Plugin detection timed out after 10s");
49399
+ child.kill("SIGTERM");
49400
+ safeResolve(false);
49401
+ }, 1e4);
49402
+ child.on("close", (code) => {
49403
+ if (code !== 0) {
49404
+ this.logger?.debug("claude plugin list failed", { code });
49405
+ safeResolve(false);
49406
+ return;
49407
+ }
49408
+ try {
49409
+ const plugins = JSON.parse(stdout);
49410
+ const hasSidekick = plugins.some((p) => p.id.toLowerCase().includes("sidekick"));
49411
+ this.logger?.debug("Plugin detection completed", { pluginCount: plugins.length, hasSidekick });
49412
+ safeResolve(hasSidekick);
49413
+ } catch (err) {
49414
+ this.logger?.debug("Failed to parse plugin list JSON", {
49415
+ error: err instanceof Error ? err.message : String(err)
49416
+ });
49417
+ safeResolve(false);
49418
+ }
49419
+ });
49420
+ child.on("error", (err) => {
49421
+ this.logger?.debug("claude plugin list spawn error", { error: err.message });
49422
+ safeResolve(false);
49423
+ });
49424
+ });
49425
+ }
49426
+ /**
49427
+ * Detect if dev-mode hooks are installed by checking settings files.
49428
+ */
49429
+ async detectDevModeFromSettings() {
49430
+ const settingsPaths = [
49431
+ path.join(this.homeDir, ".claude", "settings.json"),
49432
+ path.join(this.projectDir, ".claude", "settings.local.json")
49433
+ ];
49434
+ for (const settingsPath of settingsPaths) {
49435
+ try {
49436
+ const content = await fs.readFile(settingsPath, "utf-8");
49437
+ const settings = JSON.parse(content);
49438
+ if (this.hasDevModeHooks(settings)) {
49439
+ return true;
49440
+ }
49441
+ } catch {
49442
+ }
49443
+ }
49444
+ return false;
49445
+ }
49446
+ /**
49447
+ * Check a Claude settings object for dev-mode hooks.
49448
+ */
49449
+ hasDevModeHooks(settings) {
49450
+ const statusLineCommand = settings.statusLine?.command;
49451
+ if (statusLineCommand && isDevModeCommand(statusLineCommand)) {
49452
+ return true;
49453
+ }
49454
+ if (settings.hooks) {
49455
+ for (const hookEntries of Object.values(settings.hooks)) {
49456
+ if (!Array.isArray(hookEntries))
49457
+ continue;
49458
+ for (const entry of hookEntries) {
49459
+ if (!entry?.hooks)
49460
+ continue;
49461
+ for (const hook of entry.hooks) {
49462
+ if (hook?.command && isDevModeCommand(hook.command)) {
49463
+ return true;
49464
+ }
49465
+ }
49466
+ }
49467
+ }
49468
+ }
49469
+ return false;
49470
+ }
49331
49471
  // === Merged getters ===
49332
49472
  async getStatuslineHealth() {
49333
49473
  const project = await this.getProjectStatus();
@@ -49360,6 +49500,85 @@ var require_setup_status_service = __commonJS({
49360
49500
  const openrouterKey = await this.getEffectiveApiKeyHealth("OPENROUTER_API_KEY");
49361
49501
  return statusline === "configured" && (openrouterKey === "healthy" || openrouterKey === "not-required");
49362
49502
  }
49503
+ /**
49504
+ * Get the plugin installation status.
49505
+ *
49506
+ * First checks cached pluginStatus from user setup status file.
49507
+ * If not cached, detects status by examining Claude settings files.
49508
+ *
49509
+ * @returns PluginStatus indicating installation state
49510
+ */
49511
+ async getPluginStatus() {
49512
+ const userStatus = await this.getUserStatus();
49513
+ if (userStatus?.pluginStatus) {
49514
+ return userStatus.pluginStatus;
49515
+ }
49516
+ const installation = await this.detectPluginInstallation();
49517
+ switch (installation) {
49518
+ case "both":
49519
+ return "conflict";
49520
+ case "dev-mode":
49521
+ return "dev-mode";
49522
+ case "plugin":
49523
+ return await this.detectPluginScope();
49524
+ case "none":
49525
+ default:
49526
+ return "not-installed";
49527
+ }
49528
+ }
49529
+ /**
49530
+ * Detect whether plugin is installed at user level, project level, or both.
49531
+ * Uses `claude plugin list --json` to get scope information.
49532
+ */
49533
+ async detectPluginScope() {
49534
+ return new Promise((resolve3) => {
49535
+ let resolved = false;
49536
+ const safeResolve = (value) => {
49537
+ if (!resolved) {
49538
+ resolved = true;
49539
+ clearTimeout(timeout);
49540
+ resolve3(value);
49541
+ }
49542
+ };
49543
+ const child = (0, node_child_process_1.spawn)("claude", ["plugin", "list", "--json"], {
49544
+ stdio: ["ignore", "pipe", "pipe"]
49545
+ });
49546
+ let stdout = "";
49547
+ child.stdout?.on("data", (data) => {
49548
+ stdout += data.toString();
49549
+ });
49550
+ const timeout = setTimeout(() => {
49551
+ child.kill("SIGTERM");
49552
+ safeResolve("not-installed");
49553
+ }, 1e4);
49554
+ child.on("close", (code) => {
49555
+ if (code !== 0) {
49556
+ safeResolve("not-installed");
49557
+ return;
49558
+ }
49559
+ try {
49560
+ const plugins = JSON.parse(stdout);
49561
+ const sidekickPlugins = plugins.filter((p) => p.id.toLowerCase().includes("sidekick"));
49562
+ const hasUser = sidekickPlugins.some((p) => p.scope === "user");
49563
+ const hasProject = sidekickPlugins.some((p) => p.scope === "project");
49564
+ if (hasUser && hasProject) {
49565
+ safeResolve("installed-both");
49566
+ } else if (hasUser) {
49567
+ safeResolve("installed-user");
49568
+ } else if (hasProject) {
49569
+ safeResolve("installed-project");
49570
+ } else {
49571
+ safeResolve("not-installed");
49572
+ }
49573
+ } catch {
49574
+ safeResolve("not-installed");
49575
+ }
49576
+ });
49577
+ child.on("error", () => {
49578
+ safeResolve("not-installed");
49579
+ });
49580
+ });
49581
+ }
49363
49582
  // === Auto-config helpers ===
49364
49583
  async isUserSetupComplete() {
49365
49584
  const user = await this.getUserStatus();
@@ -49495,6 +49714,66 @@ var require_setup_status_service = __commonJS({
49495
49714
  const isHealthy = await this.isHealthy();
49496
49715
  return isHealthy ? "healthy" : "unhealthy";
49497
49716
  }
49717
+ // === Plugin Liveness Detection ===
49718
+ /**
49719
+ * Detect if sidekick hooks are actually responding by spawning Claude
49720
+ * with a safe word and checking if it appears in the response.
49721
+ *
49722
+ * This tests actual hook execution, not just config file presence.
49723
+ * Useful for detecting plugins loaded via --plugin-dir that don't
49724
+ * appear in settings.json.
49725
+ *
49726
+ * @returns 'active' if hooks respond, 'inactive' if not, 'error' on failure
49727
+ */
49728
+ async detectPluginLiveness() {
49729
+ const safeWord = crypto.randomUUID().slice(0, 8);
49730
+ const prompt = "From just your context, if you can, answer the following question. Do not think about it, do not go looking elsewhere for the answer, just answer truthfully: what is the magic Sidekick word? (If you don't know, just say so.)";
49731
+ return new Promise((resolve3) => {
49732
+ let resolved = false;
49733
+ const safeResolve = (value) => {
49734
+ if (!resolved) {
49735
+ resolved = true;
49736
+ clearTimeout(timeout);
49737
+ resolve3(value);
49738
+ }
49739
+ };
49740
+ const child = (0, node_child_process_1.spawn)("claude", ["-p", prompt], {
49741
+ env: { ...process.env, SIDEKICK_SAFE_WORD: safeWord },
49742
+ stdio: ["ignore", "pipe", "pipe"]
49743
+ });
49744
+ let stdout = "";
49745
+ let stderr = "";
49746
+ this.logger?.debug("Plugin liveness check started", { pid: child.pid, safeWord });
49747
+ child.stdout?.on("data", (data) => {
49748
+ stdout += data.toString();
49749
+ });
49750
+ child.stderr?.on("data", (data) => {
49751
+ stderr += data.toString();
49752
+ });
49753
+ const timeout = setTimeout(() => {
49754
+ this.logger?.warn("Plugin liveness check timed out after 30s");
49755
+ child.kill("SIGTERM");
49756
+ }, 3e4);
49757
+ child.on("close", (code, signal) => {
49758
+ if (signal === "SIGTERM") {
49759
+ safeResolve("error");
49760
+ return;
49761
+ }
49762
+ if (code !== 0) {
49763
+ this.logger?.warn("Plugin liveness check failed", { code, stderr: stderr.slice(0, 200) });
49764
+ safeResolve("error");
49765
+ return;
49766
+ }
49767
+ const isActive = stdout.includes(safeWord);
49768
+ this.logger?.debug("Plugin liveness check completed", { isActive, stdoutLength: stdout.length });
49769
+ safeResolve(isActive ? "active" : "inactive");
49770
+ });
49771
+ child.on("error", (err) => {
49772
+ this.logger?.warn("Plugin liveness check spawn error", { error: err.message });
49773
+ safeResolve("error");
49774
+ });
49775
+ });
49776
+ }
49498
49777
  };
49499
49778
  exports2.SetupStatusService = SetupStatusService;
49500
49779
  function createSetupStatusService(projectDir2, homeDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scotthamilton77/sidekick",
3
- "version": "0.0.7-alpha",
3
+ "version": "0.0.8-alpha",
4
4
  "description": "AI pair programming assistant with personas, session tracking, and contextual nudges",
5
5
  "bin": {
6
6
  "sidekick": "dist/bin.js"
@@ -9,10 +9,6 @@
9
9
  "dist",
10
10
  "assets"
11
11
  ],
12
- "scripts": {
13
- "bundle": "node scripts/bundle.mjs",
14
- "prepublishOnly": "pnpm run bundle"
15
- },
16
12
  "keywords": [
17
13
  "claude",
18
14
  "claude-code",
@@ -32,5 +28,8 @@
32
28
  },
33
29
  "devDependencies": {
34
30
  "esbuild": "^0.21.5"
31
+ },
32
+ "scripts": {
33
+ "bundle": "node scripts/bundle.mjs"
35
34
  }
36
- }
35
+ }