codiedev 0.9.25 → 0.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/backup/run.js +5 -0
- package/dist/captureExclusions.d.ts +30 -0
- package/dist/captureExclusions.js +118 -0
- package/dist/cli.js +31 -0
- package/dist/commands/capture.d.ts +14 -0
- package/dist/commands/capture.js +148 -0
- package/dist/connect.js +19 -0
- package/dist/hook.js +38 -2
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +26 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,12 +17,28 @@ If both are installed, hooks are wired up for both. If neither is installed, the
|
|
|
17
17
|
|
|
18
18
|
## What gets captured
|
|
19
19
|
|
|
20
|
-
-
|
|
20
|
+
- Agent sessions from connected devices (capture is enabled by default)
|
|
21
21
|
- Transcript + basic stats (message count, tool calls)
|
|
22
22
|
- Processed into summaries and embeddings server-side for search in `/portal/memories`
|
|
23
23
|
|
|
24
24
|
Each captured memory records its `source` (`claude-code` or `codex`) so you can filter in the portal.
|
|
25
25
|
|
|
26
|
+
## Keep folders local
|
|
27
|
+
|
|
28
|
+
Folder exclusions are device-specific and include every child folder. The CLI
|
|
29
|
+
writes the rule locally before attempting to sync its read-only status to the
|
|
30
|
+
portal, so an offline or failed status sync never weakens the exclusion.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
codiedev ignore ~/Obsidian
|
|
34
|
+
codiedev ignored ~/Obsidian/People # check whether a path is ignored
|
|
35
|
+
codiedev ignored # list every ignored folder
|
|
36
|
+
codiedev unignore ~/Obsidian
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run `ignore` once per device or persistent VM/container. A new ephemeral
|
|
40
|
+
container needs its own rule unless `~/.codiedev/config.json` is persisted.
|
|
41
|
+
|
|
26
42
|
## Codex note
|
|
27
43
|
|
|
28
44
|
Codex doesn't currently expose a `SessionEnd` event, so the hook runs on `Stop` (fires after each assistant turn). The backend dedupes by `(company, session_id)` and replaces the stored transcript on re-upload so the latest turn's snapshot wins. Summarization happens once per session; subsequent turn uploads keep the raw transcript fresh but don't re-summarize (to avoid running up LLM cost).
|
package/dist/backup/run.js
CHANGED
|
@@ -86,6 +86,7 @@ const child_process_1 = require("child_process");
|
|
|
86
86
|
const utils_1 = require("../utils");
|
|
87
87
|
const cache_2 = require("./cache");
|
|
88
88
|
const push = __importStar(require("../commands/push"));
|
|
89
|
+
const captureExclusions_1 = require("../captureExclusions");
|
|
89
90
|
function gitRoot(cwd) {
|
|
90
91
|
try {
|
|
91
92
|
return (0, child_process_1.execFileSync)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
@@ -143,6 +144,8 @@ function runBackup(cwd, hookScriptPath) {
|
|
|
143
144
|
const config = (0, utils_1.readConfig)();
|
|
144
145
|
if (!config)
|
|
145
146
|
return;
|
|
147
|
+
if ((0, captureExclusions_1.isCaptureIgnored)(cwd, config.captureIgnoredPaths))
|
|
148
|
+
return;
|
|
146
149
|
const root = gitRoot(cwd);
|
|
147
150
|
if (!root)
|
|
148
151
|
return;
|
|
@@ -195,6 +198,8 @@ async function runBackupUpload(jobFile, deps = DEFAULT_BACKUP_UPLOAD_DEPS) {
|
|
|
195
198
|
const config = deps.readConfig(); // same machine — read full config from disk
|
|
196
199
|
if (!config)
|
|
197
200
|
return;
|
|
201
|
+
if ((0, captureExclusions_1.isCaptureIgnored)(job.root, config.captureIgnoredPaths))
|
|
202
|
+
return;
|
|
198
203
|
const cache = deps.loadCache();
|
|
199
204
|
let dirty = false;
|
|
200
205
|
for (const f of job.files) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CodiedevConfig } from "./utils";
|
|
2
|
+
export interface NormalizeCapturePathDeps {
|
|
3
|
+
cwd?: () => string;
|
|
4
|
+
homedir?: () => string;
|
|
5
|
+
realpathSync?: (value: string) => string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Convert a user-entered path into the canonical absolute form persisted in
|
|
9
|
+
* ~/.codiedev/config.json. Existing symlinks resolve to their real target so a
|
|
10
|
+
* session cannot bypass an exclusion by entering the same folder through a
|
|
11
|
+
* different alias. Non-existent paths still normalize deterministically.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeCapturePath(input: string, deps?: NormalizeCapturePathDeps): string;
|
|
14
|
+
export declare function findMatchingCaptureExclusion(candidate: string | string[] | undefined, ignoredPaths: string[] | undefined): string | null;
|
|
15
|
+
export declare function isCaptureIgnored(candidate: string | string[] | undefined, ignoredPaths: string[] | undefined): boolean;
|
|
16
|
+
export interface CaptureScopeInput {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
workspace_roots?: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Every local root associated with a hook event, deduped in stable order. */
|
|
21
|
+
export declare function captureScopePaths(input: CaptureScopeInput, fallback?: string): string[];
|
|
22
|
+
export declare function shouldIgnoreCaptureInput(config: Pick<CodiedevConfig, "captureIgnoredPaths"> | null | undefined, input: CaptureScopeInput, fallback?: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Final egress decision for queued transcript uploads. Jobs written by older
|
|
25
|
+
* CLI versions have no scope metadata; once any exclusion exists, dropping
|
|
26
|
+
* those legacy jobs is safer than guessing that they are public.
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldSkipCaptureUploadJob(job: {
|
|
29
|
+
scopePaths?: string[];
|
|
30
|
+
}, config: Pick<CodiedevConfig, "captureIgnoredPaths"> | null | undefined): boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.normalizeCapturePath = normalizeCapturePath;
|
|
37
|
+
exports.findMatchingCaptureExclusion = findMatchingCaptureExclusion;
|
|
38
|
+
exports.isCaptureIgnored = isCaptureIgnored;
|
|
39
|
+
exports.captureScopePaths = captureScopePaths;
|
|
40
|
+
exports.shouldIgnoreCaptureInput = shouldIgnoreCaptureInput;
|
|
41
|
+
exports.shouldSkipCaptureUploadJob = shouldSkipCaptureUploadJob;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const DEFAULT_NORMALIZE_DEPS = {
|
|
46
|
+
cwd: () => process.cwd(),
|
|
47
|
+
homedir: () => os.homedir(),
|
|
48
|
+
realpathSync: (value) => fs.realpathSync.native(value),
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Convert a user-entered path into the canonical absolute form persisted in
|
|
52
|
+
* ~/.codiedev/config.json. Existing symlinks resolve to their real target so a
|
|
53
|
+
* session cannot bypass an exclusion by entering the same folder through a
|
|
54
|
+
* different alias. Non-existent paths still normalize deterministically.
|
|
55
|
+
*/
|
|
56
|
+
function normalizeCapturePath(input, deps = {}) {
|
|
57
|
+
const d = { ...DEFAULT_NORMALIZE_DEPS, ...deps };
|
|
58
|
+
const trimmed = input.trim();
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
throw new Error("Folder path is required");
|
|
61
|
+
const expanded = /^~(?=$|[\\/])/.test(trimmed)
|
|
62
|
+
? path.join(d.homedir(), trimmed.slice(1))
|
|
63
|
+
: trimmed;
|
|
64
|
+
const absolute = path.resolve(d.cwd(), expanded);
|
|
65
|
+
try {
|
|
66
|
+
return path.normalize(d.realpathSync(absolute));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return path.normalize(absolute);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function comparisonKey(value) {
|
|
73
|
+
const normalized = path.normalize(value);
|
|
74
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
75
|
+
}
|
|
76
|
+
function containsPath(root, candidate) {
|
|
77
|
+
const relative = path.relative(comparisonKey(root), comparisonKey(candidate));
|
|
78
|
+
return (relative === "" ||
|
|
79
|
+
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)));
|
|
80
|
+
}
|
|
81
|
+
function findMatchingCaptureExclusion(candidate, ignoredPaths) {
|
|
82
|
+
if (!candidate || !ignoredPaths || ignoredPaths.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const candidates = Array.isArray(candidate) ? candidate : [candidate];
|
|
85
|
+
for (const ignored of ignoredPaths) {
|
|
86
|
+
const root = normalizeCapturePath(ignored);
|
|
87
|
+
for (const value of candidates) {
|
|
88
|
+
if (containsPath(root, normalizeCapturePath(value)))
|
|
89
|
+
return root;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function isCaptureIgnored(candidate, ignoredPaths) {
|
|
95
|
+
return findMatchingCaptureExclusion(candidate, ignoredPaths) !== null;
|
|
96
|
+
}
|
|
97
|
+
/** Every local root associated with a hook event, deduped in stable order. */
|
|
98
|
+
function captureScopePaths(input, fallback = process.cwd()) {
|
|
99
|
+
const values = [input.cwd, ...(input.workspace_roots ?? [])]
|
|
100
|
+
.filter((value) => typeof value === "string" && value.length > 0);
|
|
101
|
+
return [...new Set(values.length > 0 ? values : [fallback])];
|
|
102
|
+
}
|
|
103
|
+
function shouldIgnoreCaptureInput(config, input, fallback) {
|
|
104
|
+
return isCaptureIgnored(captureScopePaths(input, fallback), config?.captureIgnoredPaths);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Final egress decision for queued transcript uploads. Jobs written by older
|
|
108
|
+
* CLI versions have no scope metadata; once any exclusion exists, dropping
|
|
109
|
+
* those legacy jobs is safer than guessing that they are public.
|
|
110
|
+
*/
|
|
111
|
+
function shouldSkipCaptureUploadJob(job, config) {
|
|
112
|
+
const ignoredPaths = config?.captureIgnoredPaths ?? [];
|
|
113
|
+
if (ignoredPaths.length === 0)
|
|
114
|
+
return false;
|
|
115
|
+
if (!job.scopePaths || job.scopePaths.length === 0)
|
|
116
|
+
return true;
|
|
117
|
+
return isCaptureIgnored(job.scopePaths, ignoredPaths);
|
|
118
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ const post_1 = require("./commands/post");
|
|
|
33
33
|
const share_1 = require("./commands/share");
|
|
34
34
|
const send_1 = require("./commands/send");
|
|
35
35
|
const react_1 = require("./commands/react");
|
|
36
|
+
const capture_1 = require("./commands/capture");
|
|
36
37
|
const version_1 = require("./version");
|
|
37
38
|
const HELP = `
|
|
38
39
|
Codiedev CLI
|
|
@@ -95,6 +96,11 @@ Feed:
|
|
|
95
96
|
[--mention <name>]... Publish to the team feed
|
|
96
97
|
codiedev react <postId> <emoji> Toggle a reaction on a feed post
|
|
97
98
|
|
|
99
|
+
Privacy:
|
|
100
|
+
codiedev ignore <path> Never capture this folder or its children
|
|
101
|
+
codiedev unignore <path> Stop ignoring a folder
|
|
102
|
+
codiedev ignored [path] List ignored folders, or check one path
|
|
103
|
+
|
|
98
104
|
Capture:
|
|
99
105
|
codiedev note "<text>" Capture a passing thought
|
|
100
106
|
|
|
@@ -130,6 +136,18 @@ Backend: ${process.env.CODIEDEV_URL ?? "https://codiedev.com"}
|
|
|
130
136
|
Token: https://codiedev.com/portal/integrations/claude-code
|
|
131
137
|
Docs: codiedev docs
|
|
132
138
|
`.trim();
|
|
139
|
+
const IGNORE_HELP = `
|
|
140
|
+
codiedev ignore — keep folders out of session capture
|
|
141
|
+
|
|
142
|
+
codiedev ignore <path> Never capture this folder or its children
|
|
143
|
+
codiedev unignore <path> Stop ignoring a folder
|
|
144
|
+
codiedev ignored List ignored folders on this device
|
|
145
|
+
codiedev ignored <path> Check whether a specific path is ignored
|
|
146
|
+
|
|
147
|
+
Exclusions are per-device and cover every child folder. The rule is written
|
|
148
|
+
locally before any capture leaves your machine, so an offline or failed portal
|
|
149
|
+
status sync never weakens it.
|
|
150
|
+
`.trim();
|
|
133
151
|
const COMMAND_HELP = {
|
|
134
152
|
connect: `
|
|
135
153
|
codiedev connect — link your agent CLI to Codiedev
|
|
@@ -237,6 +255,10 @@ codiedev note — capture a passing thought, linked to your session
|
|
|
237
255
|
codiedev note "idempotency key is worth a follow-up"
|
|
238
256
|
codiedev note "timeout threshold feels arbitrary — validate on slow networks"
|
|
239
257
|
`.trim(),
|
|
258
|
+
ignore: IGNORE_HELP,
|
|
259
|
+
unignore: IGNORE_HELP,
|
|
260
|
+
ignored: IGNORE_HELP,
|
|
261
|
+
capture: IGNORE_HELP,
|
|
240
262
|
promote: `
|
|
241
263
|
codiedev promote — promote an auto-extracted artifact to authored
|
|
242
264
|
|
|
@@ -482,6 +504,15 @@ async function main() {
|
|
|
482
504
|
case "react":
|
|
483
505
|
await (0, react_1.runReact)(rest);
|
|
484
506
|
return;
|
|
507
|
+
case "ignore":
|
|
508
|
+
case "unignore":
|
|
509
|
+
case "ignored":
|
|
510
|
+
await (0, capture_1.runCaptureCommand)([command, ...rest]);
|
|
511
|
+
return;
|
|
512
|
+
case "capture":
|
|
513
|
+
// Back-compat alias for the earlier `codiedev capture <sub>` form.
|
|
514
|
+
await (0, capture_1.runCaptureCommand)(rest);
|
|
515
|
+
return;
|
|
485
516
|
default:
|
|
486
517
|
console.error(`Unknown command: ${command}`);
|
|
487
518
|
console.error(HELP);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CodiedevConfig } from "../utils";
|
|
2
|
+
import { apiRequest } from "./shared";
|
|
3
|
+
export interface CaptureCommandDeps {
|
|
4
|
+
readConfig: () => CodiedevConfig | null;
|
|
5
|
+
writeConfig: (config: CodiedevConfig) => void;
|
|
6
|
+
apiRequest: typeof apiRequest;
|
|
7
|
+
log: (line: string) => void;
|
|
8
|
+
warn: (line: string) => void;
|
|
9
|
+
randomUUID: () => string;
|
|
10
|
+
hostname: () => string;
|
|
11
|
+
normalizePath: (value: string) => string;
|
|
12
|
+
configForRequest: (config: CodiedevConfig) => CodiedevConfig;
|
|
13
|
+
}
|
|
14
|
+
export declare function runCaptureCommand(args: string[], depsOverride?: Partial<CaptureCommandDeps>): Promise<void>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCaptureCommand = runCaptureCommand;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const captureExclusions_1 = require("../captureExclusions");
|
|
40
|
+
const utils_1 = require("../utils");
|
|
41
|
+
const shared_1 = require("./shared");
|
|
42
|
+
const DEFAULT_DEPS = {
|
|
43
|
+
readConfig: utils_1.readConfig,
|
|
44
|
+
writeConfig: utils_1.writeConfig,
|
|
45
|
+
apiRequest: shared_1.apiRequest,
|
|
46
|
+
log: console.log,
|
|
47
|
+
warn: console.warn,
|
|
48
|
+
randomUUID: () => crypto.randomUUID(),
|
|
49
|
+
hostname: () => os.hostname(),
|
|
50
|
+
normalizePath: captureExclusions_1.normalizeCapturePath,
|
|
51
|
+
configForRequest: shared_1.applyConfigEnvOverrides,
|
|
52
|
+
};
|
|
53
|
+
function requireConnectedConfig(deps) {
|
|
54
|
+
const config = deps.readConfig();
|
|
55
|
+
if (!config) {
|
|
56
|
+
throw new Error("Not connected. Run `npx codiedev connect` first.");
|
|
57
|
+
}
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
function withDeviceIdentity(config, deps) {
|
|
61
|
+
return {
|
|
62
|
+
...config,
|
|
63
|
+
deviceId: config.deviceId || deps.randomUUID(),
|
|
64
|
+
deviceName: config.deviceName || deps.hostname(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function syncExclusions(config, deps) {
|
|
68
|
+
try {
|
|
69
|
+
await deps.apiRequest("POST", "/api/cli/capture-exclusions", {
|
|
70
|
+
config: deps.configForRequest(config),
|
|
71
|
+
body: {
|
|
72
|
+
deviceId: config.deviceId,
|
|
73
|
+
deviceName: config.deviceName,
|
|
74
|
+
ignoredPaths: config.captureIgnoredPaths ?? [],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
deps.log("Portal status synced.");
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
deps.warn(`Privacy setting saved locally, but portal status did not sync: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function requirePath(args) {
|
|
84
|
+
const value = args.slice(1).join(" ").trim();
|
|
85
|
+
if (!value) {
|
|
86
|
+
throw new Error("Usage: codiedev ignore <path> | codiedev unignore <path>");
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function reportCheck(rawPath, current, deps) {
|
|
91
|
+
const target = deps.normalizePath(rawPath);
|
|
92
|
+
const match = (0, captureExclusions_1.findMatchingCaptureExclusion)(target, current.captureIgnoredPaths);
|
|
93
|
+
if (match) {
|
|
94
|
+
deps.log(`IGNORED: ${target}`);
|
|
95
|
+
deps.log(`Matched exclusion: ${match}`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
deps.log(`CAPTURED: ${target}`);
|
|
99
|
+
deps.log("No ignored-folder rule matches this path.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function runCaptureCommand(args, depsOverride = {}) {
|
|
103
|
+
const deps = { ...DEFAULT_DEPS, ...depsOverride };
|
|
104
|
+
const subcommand = args[0];
|
|
105
|
+
const current = requireConnectedConfig(deps);
|
|
106
|
+
if (subcommand === "ignore") {
|
|
107
|
+
const target = deps.normalizePath(requirePath(args));
|
|
108
|
+
const existing = (current.captureIgnoredPaths ?? []).map(deps.normalizePath);
|
|
109
|
+
const nextPaths = existing.includes(target) ? existing : [...existing, target];
|
|
110
|
+
const next = withDeviceIdentity({ ...current, captureIgnoredPaths: nextPaths }, deps);
|
|
111
|
+
deps.writeConfig(next);
|
|
112
|
+
deps.log(`Ignoring ${target} and all child folders.`);
|
|
113
|
+
await syncExclusions(next, deps);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (subcommand === "unignore") {
|
|
117
|
+
const target = deps.normalizePath(requirePath(args));
|
|
118
|
+
const existing = (current.captureIgnoredPaths ?? []).map(deps.normalizePath);
|
|
119
|
+
const next = withDeviceIdentity({ ...current, captureIgnoredPaths: existing.filter((value) => value !== target) }, deps);
|
|
120
|
+
deps.writeConfig(next);
|
|
121
|
+
deps.log(`No longer ignoring ${target}.`);
|
|
122
|
+
await syncExclusions(next, deps);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (subcommand === "ignored") {
|
|
126
|
+
// `ignored` lists everything; `ignored <path>` answers "is this ignored?".
|
|
127
|
+
const query = args.slice(1).join(" ").trim();
|
|
128
|
+
if (query) {
|
|
129
|
+
reportCheck(query, current, deps);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const paths = current.captureIgnoredPaths ?? [];
|
|
133
|
+
if (paths.length === 0) {
|
|
134
|
+
deps.log("No ignored folders configured on this device.");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
deps.log("Ignored folders:");
|
|
138
|
+
for (const value of paths)
|
|
139
|
+
deps.log(` ${value}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Back-compat: the earlier `codiedev capture check <path>` form.
|
|
143
|
+
if (subcommand === "check") {
|
|
144
|
+
reportCheck(requirePath(args), current, deps);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unknown command: ${subcommand ?? "(missing)"}. Use ignore, unignore, or ignored.`);
|
|
148
|
+
}
|
package/dist/connect.js
CHANGED
|
@@ -37,6 +37,8 @@ exports.runConnect = runConnect;
|
|
|
37
37
|
const readline = __importStar(require("readline"));
|
|
38
38
|
const https = __importStar(require("https"));
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
40
42
|
const utils_1 = require("./utils");
|
|
41
43
|
const connectFlow_1 = require("./connectFlow");
|
|
42
44
|
const version_1 = require("./version");
|
|
@@ -210,6 +212,9 @@ async function runConnect(args = []) {
|
|
|
210
212
|
repos,
|
|
211
213
|
lastSynced: new Date().toISOString(),
|
|
212
214
|
backendUrl: BACKEND_URL,
|
|
215
|
+
deviceId: saved?.deviceId || crypto.randomUUID(),
|
|
216
|
+
deviceName: saved?.deviceName || os.hostname(),
|
|
217
|
+
captureIgnoredPaths: saved?.captureIgnoredPaths ?? [],
|
|
213
218
|
};
|
|
214
219
|
try {
|
|
215
220
|
(0, utils_1.writeConfig)(config);
|
|
@@ -223,6 +228,20 @@ async function runConnect(args = []) {
|
|
|
223
228
|
// a timeline when a customer says "it connected but it doesn't work."
|
|
224
229
|
// Fire-and-forget — never blocks the connect flow.
|
|
225
230
|
(0, shared_1.recordInstallEvent)(config, { step: "started", ok: true });
|
|
231
|
+
// One-way device status sync. Local config remains authoritative: a network
|
|
232
|
+
// failure here never removes or weakens an ignored path. This also registers
|
|
233
|
+
// a freshly connected device with an empty list so the portal can distinguish
|
|
234
|
+
// "connected, no exclusions" from "old CLI has never reported status".
|
|
235
|
+
try {
|
|
236
|
+
await postJson(`${BACKEND_URL}/api/cli/capture-exclusions`, {
|
|
237
|
+
deviceId: config.deviceId,
|
|
238
|
+
deviceName: config.deviceName,
|
|
239
|
+
ignoredPaths: config.captureIgnoredPaths,
|
|
240
|
+
}, { Authorization: `Bearer ${token}` });
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
console.warn(`Warning: Local capture settings are active, but portal status did not sync — ${err.message}`);
|
|
244
|
+
}
|
|
226
245
|
// Migration: strip MCP server entries written by pre-CLI-only versions
|
|
227
246
|
// of `codiedev connect`. These pointed at `npx codiedev-mcp`, a binary
|
|
228
247
|
// that was removed in 0.3.4 and that 0.7.11+ no longer ships. Without
|
package/dist/hook.js
CHANGED
|
@@ -45,6 +45,7 @@ const utils_1 = require("./utils");
|
|
|
45
45
|
const limitEvent_1 = require("./limitEvent");
|
|
46
46
|
const version_1 = require("./version");
|
|
47
47
|
const captureLog_1 = require("./captureLog");
|
|
48
|
+
const captureExclusions_1 = require("./captureExclusions");
|
|
48
49
|
function getJson(url, bearerToken) {
|
|
49
50
|
return new Promise((resolve, reject) => {
|
|
50
51
|
const parsed = new URL(url);
|
|
@@ -368,6 +369,12 @@ async function main() {
|
|
|
368
369
|
// Write per-session upload state so the throttle can defer later
|
|
369
370
|
// non-final uploads within the same window.
|
|
370
371
|
writeSessionState: utils_1.writeSessionUploadState,
|
|
372
|
+
// Re-read local config in the uploader so adding an exclusion after a
|
|
373
|
+
// job was queued still prevents its transcript from leaving the device.
|
|
374
|
+
shouldSkipJob: (job) => {
|
|
375
|
+
const config = (0, utils_1.readConfig)();
|
|
376
|
+
return (0, captureExclusions_1.shouldSkipCaptureUploadJob)(job, config);
|
|
377
|
+
},
|
|
371
378
|
};
|
|
372
379
|
}
|
|
373
380
|
// Detached uploader child (spawned by the capture path below). The job —
|
|
@@ -441,6 +448,13 @@ async function main() {
|
|
|
441
448
|
catch {
|
|
442
449
|
process.exit(0);
|
|
443
450
|
}
|
|
451
|
+
// Local privacy boundary: every stdin-driven hook mode passes through this
|
|
452
|
+
// check BEFORE prompt discovery, limit-event tails, SessionStart pings,
|
|
453
|
+
// backup, self-heal, transcript reads, or detached dispatch. Ignored means
|
|
454
|
+
// no Codiedev request at all for this session.
|
|
455
|
+
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
456
|
+
process.exit(0);
|
|
457
|
+
}
|
|
444
458
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
445
459
|
if (process.argv[2] === "discover") {
|
|
446
460
|
await runDiscover(hookInput.prompt);
|
|
@@ -563,6 +577,17 @@ async function runCaptureFinish(input) {
|
|
|
563
577
|
const config = (0, utils_1.readConfig)();
|
|
564
578
|
if (!config)
|
|
565
579
|
return;
|
|
580
|
+
// Re-check in the detached child: the user may have added an exclusion
|
|
581
|
+
// after the parent hook dispatched this job but before it started.
|
|
582
|
+
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)(config, input)) {
|
|
583
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
584
|
+
stage: "capture-finish",
|
|
585
|
+
sessionId: input.session_id,
|
|
586
|
+
outcome: "skip",
|
|
587
|
+
detail: "ignored-path",
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
566
591
|
const { session_id, transcript_path, cwd, workspace_roots } = input;
|
|
567
592
|
if (!transcript_path) {
|
|
568
593
|
await postSessionSkip(config, session_id, "no-transcript-path");
|
|
@@ -588,6 +613,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
588
613
|
const workspaceRoot = workspace_roots && workspace_roots.length > 0
|
|
589
614
|
? workspace_roots[0]
|
|
590
615
|
: cwd || process.cwd();
|
|
616
|
+
const scopePaths = (0, captureExclusions_1.captureScopePaths)({ cwd, workspace_roots }, workspaceRoot);
|
|
591
617
|
let transcriptContent;
|
|
592
618
|
try {
|
|
593
619
|
transcriptContent = fs.readFileSync(transcript_path, "utf8");
|
|
@@ -718,7 +744,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
718
744
|
const hookScriptPath = process.argv[1];
|
|
719
745
|
const tmpFile = path.join(os.tmpdir(), `codiedev-capture-${session_id ?? process.pid}.json`);
|
|
720
746
|
if (decision.mode === "immediate") {
|
|
721
|
-
(0, utils_1.dispatchCaptureDetached)({
|
|
747
|
+
(0, utils_1.dispatchCaptureDetached)({
|
|
748
|
+
url: `${config.backendUrl}/api/captureSession`,
|
|
749
|
+
token: config.token,
|
|
750
|
+
body: payload,
|
|
751
|
+
scopePaths,
|
|
752
|
+
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync });
|
|
722
753
|
// Retry any earlier capture jobs that failed to upload (transient network
|
|
723
754
|
// blips), excluding the one we just dispatched. Best-effort, detached.
|
|
724
755
|
// Skip the sweep on deferred path — the deferred child will handle uploading.
|
|
@@ -754,7 +785,12 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
754
785
|
// Deferred: write the job file (newer turns overwrite = supersede), then
|
|
755
786
|
// spawn the deferred-upload child which will sleep, claim, and upload.
|
|
756
787
|
// Same stdio/fd-hygiene contract as dispatchCaptureDetached.
|
|
757
|
-
(0, utils_1.dispatchDeferredUpload)({
|
|
788
|
+
(0, utils_1.dispatchDeferredUpload)({
|
|
789
|
+
url: `${config.backendUrl}/api/captureSession`,
|
|
790
|
+
token: config.token,
|
|
791
|
+
body: payload,
|
|
792
|
+
scopePaths,
|
|
793
|
+
}, hookScriptPath, { spawn: child_process_1.spawn, writeFileSync: fs.writeFileSync, tmpFile, openSync: fs.openSync, closeSync: fs.closeSync }, decision.delayMs);
|
|
758
794
|
}
|
|
759
795
|
}
|
|
760
796
|
catch (err) {
|
package/dist/utils.d.ts
CHANGED
|
@@ -12,9 +12,14 @@ export interface CodiedevConfig {
|
|
|
12
12
|
}>;
|
|
13
13
|
lastSynced: string;
|
|
14
14
|
backendUrl: string;
|
|
15
|
+
deviceId?: string;
|
|
16
|
+
deviceName?: string;
|
|
17
|
+
captureIgnoredPaths?: string[];
|
|
15
18
|
}
|
|
16
19
|
export declare function readConfig(): CodiedevConfig | null;
|
|
17
20
|
export declare function writeConfig(config: CodiedevConfig): void;
|
|
21
|
+
/** Exported for permission-focused tests; normal callers use writeConfig. */
|
|
22
|
+
export declare function writeConfigAtPath(config: CodiedevConfig, configDir: string, configPath: string): void;
|
|
18
23
|
export declare function parseGitRemoteFromConfig(configText: string): string | null;
|
|
19
24
|
export declare function readGitRemoteFromConfig(startDir: string): string | null;
|
|
20
25
|
export declare function getGitRemoteUrl(cwd: string): string | null;
|
|
@@ -171,6 +176,7 @@ export interface CaptureUploadJob {
|
|
|
171
176
|
body: Record<string, unknown>;
|
|
172
177
|
attempts?: number;
|
|
173
178
|
queuedAt?: number;
|
|
179
|
+
scopePaths?: string[];
|
|
174
180
|
}
|
|
175
181
|
export declare const MAX_UPLOAD_ATTEMPTS = 5;
|
|
176
182
|
export declare const UPLOAD_TTL_MS: number;
|
|
@@ -262,6 +268,7 @@ interface UploadDeps {
|
|
|
262
268
|
recordSuccess?: (job: CaptureUploadJob) => void;
|
|
263
269
|
log?: (entry: CaptureLogEntry) => void;
|
|
264
270
|
writeSessionState?: (sessionId: string, state: SessionUploadState) => void;
|
|
271
|
+
shouldSkipJob?: (job: CaptureUploadJob) => boolean;
|
|
265
272
|
}
|
|
266
273
|
export interface LastCaptureRecord {
|
|
267
274
|
sessionId: string | undefined;
|
package/dist/utils.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.DIRECT_UPLOAD_THRESHOLD_CHARS = exports.SWEEP_MIN_AGE_MS = exports.CAPTURE_THROTTLE_MS = exports.FINISH_ORPHAN_AGE_MS = exports.CLAIM_STALE_MS = exports.UPLOAD_TTL_MS = exports.MAX_UPLOAD_ATTEMPTS = void 0;
|
|
37
37
|
exports.readConfig = readConfig;
|
|
38
38
|
exports.writeConfig = writeConfig;
|
|
39
|
+
exports.writeConfigAtPath = writeConfigAtPath;
|
|
39
40
|
exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
|
|
40
41
|
exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
|
|
41
42
|
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
@@ -122,10 +123,19 @@ function readConfig() {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
function writeConfig(config) {
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
writeConfigAtPath(config, CONFIG_DIR, CONFIG_PATH);
|
|
127
|
+
}
|
|
128
|
+
/** Exported for permission-focused tests; normal callers use writeConfig. */
|
|
129
|
+
function writeConfigAtPath(config, configDir, configPath) {
|
|
130
|
+
if (!fs.existsSync(configDir)) {
|
|
131
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
127
132
|
}
|
|
128
|
-
fs.writeFileSync(
|
|
133
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
mode: 0o600,
|
|
136
|
+
});
|
|
137
|
+
// writeFile's mode does not replace permissions on a pre-existing file.
|
|
138
|
+
fs.chmodSync(configPath, 0o600);
|
|
129
139
|
}
|
|
130
140
|
// Parse a remote URL out of git-config INI text WITHOUT invoking git. Prefers
|
|
131
141
|
// the "origin" remote; falls back to the first remote found. Returns null if
|
|
@@ -2199,6 +2209,19 @@ async function runCaptureUpload(jobFile, deps) {
|
|
|
2199
2209
|
return;
|
|
2200
2210
|
}
|
|
2201
2211
|
const sessionId = job.body?.sessionId;
|
|
2212
|
+
if (deps.shouldSkipJob?.(job)) {
|
|
2213
|
+
if (deps.log) {
|
|
2214
|
+
try {
|
|
2215
|
+
deps.log({ stage: "upload", sessionId, outcome: "skip", detail: "ignored-path" });
|
|
2216
|
+
}
|
|
2217
|
+
catch { /* log is best-effort */ }
|
|
2218
|
+
}
|
|
2219
|
+
try {
|
|
2220
|
+
deps.unlinkSync(jobFile);
|
|
2221
|
+
}
|
|
2222
|
+
catch { /* gone */ }
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2202
2225
|
try {
|
|
2203
2226
|
// Big transcript + capable deps → direct-to-storage: fetch a one-time
|
|
2204
2227
|
// upload URL, PUT the raw bytes, send captureSession the storageId. Any
|
package/package.json
CHANGED