@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 +21 -0
- package/assets/sidekick/reminders/safe-word-liveness.yaml +13 -0
- package/assets/sidekick/reminders/user-prompt-submit.yaml +2 -5
- package/dist/bin.js +417 -14
- package/dist/daemon.js +281 -2
- package/package.json +5 -6
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
70655
|
-
disable Remove dev-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
}
|