codiedev 0.9.59 → 0.9.61

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.
@@ -8,12 +8,33 @@ export type CaptureSignals = {
8
8
  activeMs: number;
9
9
  idleThresholdMs: number;
10
10
  skillInvocations: string[];
11
+ /** How many times each invoked skill ran (each Skill tool call and each
12
+ * slash command): three review runs in one session count three. Absent
13
+ * from older CLIs, which only sent the unique set above. */
14
+ skillInvocationCounts?: Array<{
15
+ skill: string;
16
+ count: number;
17
+ }>;
11
18
  skillPulls: string[];
12
19
  toolCalls: Array<{
13
20
  name: string;
14
21
  calls: number;
15
22
  }>;
23
+ /** How the harness was launched, raw: Claude Code's "cli" / "sdk-cli",
24
+ * Codex's "codex:cli" / "codex:exec". Absent when the harness records none. */
25
+ entrypoints?: string[];
26
+ /** Claude Code subagent transcripts folded into these signals. */
27
+ subagents?: {
28
+ count: number;
29
+ truncated: boolean;
30
+ };
31
+ /** GitHub Copilot subscription usage: premium requests and nano-AIU, from
32
+ * the session's latest usage checkpoint or its shutdown. Not dollars. */
33
+ premiumRequests?: number;
34
+ nanoAiu?: number;
16
35
  truncated: boolean;
17
36
  };
18
37
  /** Compact diagnostic inputs only: never persist prompts, commands or tool output. */
19
38
  export declare function parseCaptureSignals(content: string, source?: string): CaptureSignals;
39
+ /** Add a launch value the hook detected (harnesses that do not record one). */
40
+ export declare function withLaunch(signals: CaptureSignals, launch: string | undefined): CaptureSignals;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CAPTURE_SIGNALS_VERSION = void 0;
4
4
  exports.parseCaptureSignals = parseCaptureSignals;
5
+ exports.withLaunch = withLaunch;
5
6
  const transcriptModels_1 = require("./transcriptModels");
6
7
  exports.CAPTURE_SIGNALS_VERSION = 1;
7
8
  const IDLE_MS = 5 * 60_000;
@@ -17,9 +18,12 @@ const skillKey = (value) => typeof value === "string"
17
18
  /** Compact diagnostic inputs only: never persist prompts, commands or tool output. */
18
19
  function parseCaptureSignals(content, source) {
19
20
  const timestamps = new Set();
20
- const invocations = new Set(), pulls = new Set();
21
+ const invocationCounts = new Map();
22
+ const invocations = new Set(), pulls = new Set(), entrypoints = new Set();
21
23
  const tools = new Map(), calls = new Set();
22
24
  let parsedRecords = 0, truncated = false;
25
+ let premiumRequests, nanoAiu;
26
+ let cursorSubagents;
23
27
  const addSkill = (set, value) => {
24
28
  const key = skillKey(value);
25
29
  if (!key || key.length > 200 || !/^[a-z0-9][a-z0-9_:/.-]*$/i.test(key))
@@ -29,6 +33,8 @@ function parseCaptureSignals(content, source) {
29
33
  return;
30
34
  }
31
35
  set.add(key);
36
+ if (set === invocations)
37
+ invocationCounts.set(key, (invocationCounts.get(key) ?? 0) + 1);
32
38
  };
33
39
  const command = (value) => {
34
40
  if (typeof value !== "string")
@@ -66,6 +72,37 @@ function parseCaptureSignals(content, source) {
66
72
  continue;
67
73
  const e = record;
68
74
  parsedRecords++;
75
+ const entrypoint = (value) => {
76
+ if (typeof value === "string" &&
77
+ entrypoints.size < 10 &&
78
+ /^[a-z0-9][a-z0-9_.:-]{0,39}$/i.test(value))
79
+ entrypoints.add(value.toLowerCase());
80
+ };
81
+ // Claude Code: "cli" at the terminal, "sdk-cli" for `claude -p` / SDK.
82
+ entrypoint(e.entrypoint);
83
+ // Codex: session_meta.source is "cli", "vscode", "exec" (`codex exec`),
84
+ // "mcp", or an object for sub-agents. Namespaced so values never collide.
85
+ if (e.type === "session_meta" && e.payload) {
86
+ const src = e.payload.source;
87
+ entrypoint(typeof src === "string"
88
+ ? `codex:${src}`
89
+ : src && typeof src === "object"
90
+ ? `codex:${Object.keys(src)[0] ?? "other"}`
91
+ : undefined);
92
+ }
93
+ if (e.type === "cursor.subagent_summary" && Number.isSafeInteger(e.count) && e.count > 0) {
94
+ cursorSubagents = { count: e.count, truncated: e.truncated === true };
95
+ }
96
+ // GitHub Copilot (normalized): one skill.invoked event per skill run,
97
+ // whether the user or the agent invoked it.
98
+ if (e.type === "copilot.skill_invoked")
99
+ addSkill(invocations, e.skill);
100
+ if (e.type === "copilot.usage_summary") {
101
+ if (typeof e.premiumRequests === "number" && e.premiumRequests >= 0)
102
+ premiumRequests = e.premiumRequests;
103
+ if (typeof e.nanoAiu === "number" && e.nanoAiu >= 0)
104
+ nanoAiu = e.nanoAiu;
105
+ }
69
106
  const time = typeof e.timestamp === "string"
70
107
  ? Date.parse(e.timestamp)
71
108
  : e.info?.time?.created;
@@ -110,10 +147,24 @@ function parseCaptureSignals(content, source) {
110
147
  activeMs,
111
148
  idleThresholdMs: IDLE_MS,
112
149
  skillInvocations: [...invocations].sort(),
150
+ skillInvocationCounts: [...invocationCounts]
151
+ .map(([skill, count]) => ({ skill, count }))
152
+ .sort((a, b) => a.skill.localeCompare(b.skill)),
113
153
  skillPulls: [...pulls].sort(),
114
154
  toolCalls: [...tools]
115
155
  .map(([name, calls]) => ({ name, calls }))
116
156
  .sort((a, b) => a.name.localeCompare(b.name)),
157
+ ...(cursorSubagents ? { subagents: cursorSubagents } : {}),
158
+ ...(entrypoints.size ? { entrypoints: [...entrypoints].sort() } : {}),
159
+ ...(premiumRequests !== undefined ? { premiumRequests } : {}),
160
+ ...(nanoAiu !== undefined ? { nanoAiu } : {}),
117
161
  truncated,
118
162
  };
119
163
  }
164
+ /** Add a launch value the hook detected (harnesses that do not record one). */
165
+ function withLaunch(signals, launch) {
166
+ if (!launch || !/^[a-z0-9][a-z0-9_.:-]{0,39}$/.test(launch))
167
+ return signals;
168
+ const all = new Set([...(signals.entrypoints ?? []), launch]);
169
+ return all.size > 10 ? signals : { ...signals, entrypoints: [...all].sort() };
170
+ }
package/dist/cli.js CHANGED
@@ -42,7 +42,8 @@ Codiedev CLI
42
42
  someone left off across sessions and providers.
43
43
 
44
44
  Connect:
45
- codiedev connect Link Claude Code / Codex / Cursor to your org
45
+ codiedev connect Link Claude Code / Codex / Cursor /
46
+ GitHub Copilot to your org
46
47
  codiedev doctor Verify your setup after connecting
47
48
 
48
49
  Harness:
@@ -163,6 +164,7 @@ codiedev connect — link your agent CLI to Codiedev
163
164
  - ~/.codex/AGENTS.md instructions (same, for Codex)
164
165
  - ~/.cursor/hooks.json hooks (session, prompt, security, capture)
165
166
  - ~/.cursor/rules/codiedev.mdc (teaches Cursor the commands)
167
+ - ~/.copilot/hooks/codiedev.json (GitHub Copilot: VS Code + CLI capture)
166
168
 
167
169
  Re-run anytime to swap accounts or refresh the instruction block.
168
170
 
@@ -45,6 +45,7 @@ const version_1 = require("../version");
45
45
  const upgradeNudge_1 = require("../upgradeNudge");
46
46
  const captureReadiness_1 = require("../captureReadiness");
47
47
  const captureLog_1 = require("../captureLog");
48
+ const copilotInstallation_1 = require("../copilotInstallation");
48
49
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
49
50
  const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
50
51
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
@@ -482,6 +483,16 @@ async function runDoctor(_args) {
482
483
  detail: "not installed on this machine",
483
484
  });
484
485
  }
486
+ if ((0, utils_1.copilotInstalled)()) {
487
+ const hooks = (0, copilotInstallation_1.inspectCopilotHooks)();
488
+ checks.push({ name: "GitHub Copilot capture hooks", status: hooks.status === "pass" ? "pass" : "fail", detail: hooks.detail });
489
+ checks.push({ name: "GitHub Copilot CLI instructions",
490
+ status: hasCodiedevInstructions((0, utils_1.copilotInstructionsPath)()) ? "pass" : "fail",
491
+ detail: hasCodiedevInstructions((0, utils_1.copilotInstructionsPath)()) ? (0, utils_1.copilotInstructionsPath)() : "missing — re-run `codiedev connect`" });
492
+ }
493
+ else {
494
+ checks.push({ name: "GitHub Copilot", status: "warn", detail: "not installed on this machine" });
495
+ }
485
496
  // 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
486
497
  // specific PR/MR URL; branch mode works without either).
487
498
  checks.push({
package/dist/connect.js CHANGED
@@ -271,10 +271,12 @@ async function runConnect(args = []) {
271
271
  const hasCodex = (0, utils_1.codexInstalled)();
272
272
  const hasCursor = (0, utils_1.cursorInstalled)();
273
273
  const hasOpenCode = (0, utils_1.openCodeInstalled)();
274
+ const hasCopilot = (0, utils_1.copilotInstalled)();
274
275
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "claude-code", ok: hasClaude });
275
276
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "codex", ok: hasCodex });
276
277
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "cursor", ok: hasCursor });
277
278
  (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "opencode", ok: hasOpenCode });
279
+ (0, shared_1.recordInstallEvent)(config, { step: "detect", target: "copilot", ok: hasCopilot });
278
280
  const installed = [];
279
281
  // Machine-readable target keys reported back to the backend via
280
282
  // /api/cli/markInstalled so the portal's "Connected" state reflects
@@ -313,6 +315,12 @@ async function runConnect(args = []) {
313
315
  runInstall("cursor-hook", utils_1.installCursorHook, "Cursor sessionStart + stop backup + sessionEnd capture hooks (~/.cursor/hooks.json)", "cursor-hook");
314
316
  runInstall("cursor-instructions", utils_1.installCursorInstructions, "Cursor agent instructions (~/.cursor/rules/codiedev.mdc)", "cursor-instructions");
315
317
  }
318
+ // One user-level hook file covers both VS Code Copilot Chat and the Copilot
319
+ // CLI (they share the runtime and its hooks).
320
+ if (hasCopilot) {
321
+ runInstall("copilot-hook", () => (0, utils_1.installCopilotHook)(), `GitHub Copilot (VS Code + CLI) sessionStart + agentStop backup + sessionEnd capture hooks (${(0, utils_1.copilotHooksPath)()})`, "copilot-hook");
322
+ runInstall("copilot-instructions", utils_1.installCopilotInstructions, "GitHub Copilot CLI team artifact instructions", "copilot-instructions");
323
+ }
316
324
  // Always stage the adapter. It is dormant until OpenCode starts, which lets
317
325
  // a user connect Claude Code today and install the Codie harness tomorrow
318
326
  // without needing a second setup ritual.
@@ -339,7 +347,7 @@ async function runConnect(args = []) {
339
347
  });
340
348
  }
341
349
  }
342
- if (!hasClaude && !hasCodex && !hasCursor && !hasOpenCode) {
350
+ if (!hasClaude && !hasCodex && !hasCursor && !hasOpenCode && !hasCopilot) {
343
351
  console.warn("\nNo existing AI client detected; the Codie/OpenCode adapter is ready for a later install.");
344
352
  console.warn("If you just installed Claude Code, launch it once (so ~/.claude is created),");
345
353
  console.warn("then re-run `npx codiedev connect` to wire up the capture hook.");
@@ -0,0 +1,85 @@
1
+ /**
2
+ * GitHub Copilot: VS Code Copilot Chat's agent and the Copilot CLI run on the
3
+ * same runtime and write the same session record,
4
+ * `$COPILOT_HOME/session-state/<sessionId>/events.jsonl` (plus
5
+ * `workspace.yaml`), and both read user hooks from `$COPILOT_HOME/hooks/*.json`.
6
+ * Everything Copilot-specific is translated here, at the process boundary, so
7
+ * the shared capture pipeline only ever sees the Claude-shaped transcript it
8
+ * already parses.
9
+ */
10
+ /** `$COPILOT_HOME`, else `~/.copilot` (the runtime honours the same variable). */
11
+ export declare function copilotHome(env?: NodeJS.ProcessEnv, home?: string): string;
12
+ export declare function copilotSessionDir(sessionId: string, home?: string): string | undefined;
13
+ /** sessionEnd carries no transcript path; the session id names the file. */
14
+ export declare function copilotTranscriptPath(sessionId: string | undefined, home?: string): string | undefined;
15
+ export type CopilotWorkspace = {
16
+ id?: string;
17
+ cwd?: string;
18
+ gitRoot?: string;
19
+ branch?: string;
20
+ /** "vscode-agent-host" (VS Code Copilot Chat) or "github/cli" (Copilot CLI). */
21
+ clientName?: string;
22
+ };
23
+ /** workspace.yaml is flat `key: value` lines; no YAML dependency needed. */
24
+ export declare function parseCopilotWorkspace(yaml: string): CopilotWorkspace;
25
+ /** The workspace.yaml beside a transcript (or for a session id); {} if absent. */
26
+ export declare function readCopilotWorkspace(transcriptPathOrDir: string | undefined): CopilotWorkspace;
27
+ export declare const COPILOT_VSCODE_CLIENT = "vscode-agent-host";
28
+ /**
29
+ * How the session was launched. VS Code names itself in workspace.yaml; the
30
+ * CLI doesn't record whether it ran headless, so that comes from the process
31
+ * that launched the hook (`detected`, see launchContext). Unknown stays
32
+ * unknown rather than guessing "human".
33
+ */
34
+ export declare function copilotLaunch(clientName: string | undefined, detected: string | undefined): string | undefined;
35
+ /**
36
+ * Copilot's hook payloads are camelCase (`sessionId`, `transcriptPath`,
37
+ * `toolName`/`toolArgs`/`toolResult`); preToolUse in the CLI batches calls as
38
+ * `toolCalls: [{id, name, args}]`. Translate to the shared snake_case shape.
39
+ * The FIRST tool call fills tool_name/tool_input; `copilotToolCalls` keeps all
40
+ * of them so security can check each.
41
+ */
42
+ export declare function normalizeCopilotHookInput<T extends Record<string, unknown>>(input: T, home?: string): T & Record<string, unknown>;
43
+ export type CopilotToolCall = {
44
+ id?: string;
45
+ name: string;
46
+ input: unknown;
47
+ };
48
+ /** Copilot's preToolUse deny output. */
49
+ export declare function copilotPreToolBlockResponse(reason: string): {
50
+ permissionDecision: "deny";
51
+ permissionDecisionReason: string;
52
+ };
53
+ /** Only a byte-empty record is safe to discard (same rule as Cursor). */
54
+ export declare function isCopilotTranscriptEmpty(content: string): boolean;
55
+ /** session.shutdown carries the per-model token totals; written after sessionEnd. */
56
+ export declare function hasCopilotShutdown(content: string): boolean;
57
+ export type NormalizedCopilotTranscript = {
58
+ /** Claude-shaped JSONL for the shared parsers and the upload. */
59
+ content: string;
60
+ /** Fleet-mode sub-agents run inside this file (subagent.started). */
61
+ subagents: {
62
+ count: number;
63
+ truncated: boolean;
64
+ };
65
+ /** True once session.shutdown (token totals) has been written. */
66
+ final: boolean;
67
+ };
68
+ /**
69
+ * events.jsonl → Claude-shaped JSONL:
70
+ * session.start → system record (cwd, branch, entrypoint for VS Code)
71
+ * user.message → user text
72
+ * assistant.message → assistant text + tool_use blocks (model on every call)
73
+ * tool.execution_complete→ user tool_result (git output feeds branch/sha joins)
74
+ * skill.invoked → copilot.skill_invoked (one skill run)
75
+ * session.shutdown → copilot.usage per model (tokens, sub-agents
76
+ * included; requests counted from the assistant
77
+ * calls, so 0 here) + copilot.usage_summary
78
+ * session.usage_checkpoint → copilot.usage_summary (premium requests and
79
+ * nano-AIU so far; superseded by shutdown)
80
+ * subagent.started → counted as a fleet sub-agent
81
+ * Hooks, permissions, system prompts and checkpoints are dropped.
82
+ */
83
+ export declare function normalizeCopilotTranscript(raw: string, opts?: {
84
+ clientName?: string;
85
+ }): NormalizedCopilotTranscript;