@hncode/hncode 0.4.2 → 0.5.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 (65) hide show
  1. package/README.md +1 -15
  2. package/README_CN.md +1 -15
  3. package/bin/hncode +22 -9
  4. package/package.json +3 -2
  5. package/src/agent.js +149 -35
  6. package/src/bootstrap.js +56 -0
  7. package/src/btw-panel.js +224 -0
  8. package/src/cache.js +3 -2
  9. package/src/cell-buffer.js +173 -0
  10. package/src/ci.js +4 -1
  11. package/src/clipboard.js +202 -43
  12. package/src/colors.js +478 -242
  13. package/src/config.js +409 -40
  14. package/src/context-bar.js +237 -0
  15. package/src/doctor.js +518 -0
  16. package/src/experiments.js +122 -0
  17. package/src/file-history.js +112 -0
  18. package/src/file-tree.js +255 -0
  19. package/src/flush-tick.js +56 -0
  20. package/src/fork.js +44 -0
  21. package/src/fx.js +148 -0
  22. package/src/index.js +123 -11
  23. package/src/keybindings.js +229 -0
  24. package/src/llm.js +355 -34
  25. package/src/lsp.js +401 -0
  26. package/src/memory-profile.js +247 -0
  27. package/src/notebook.js +232 -0
  28. package/src/output-styles.js +183 -0
  29. package/src/patch.js +248 -0
  30. package/src/permissions.js +130 -1
  31. package/src/plan.js +3 -2
  32. package/src/plugin.js +611 -58
  33. package/src/prompt-presets.js +1 -1
  34. package/src/registry-browser.js +31 -13
  35. package/src/remote.js +20 -2
  36. package/src/rollout.js +151 -0
  37. package/src/row-cache.js +148 -0
  38. package/src/schedule.js +294 -0
  39. package/src/session-approvals.js +86 -0
  40. package/src/session-hub.js +1 -0
  41. package/src/session.js +178 -8
  42. package/src/settings-schema.js +301 -0
  43. package/src/shell-safety.js +473 -0
  44. package/src/side-thread.js +240 -0
  45. package/src/skills.js +27 -0
  46. package/src/stream-buffer.js +350 -0
  47. package/src/subagent-control.js +160 -0
  48. package/src/teleport.js +260 -0
  49. package/src/term-caps.js +411 -0
  50. package/src/term.js +283 -8
  51. package/src/tool-result.js +264 -236
  52. package/src/tools/agent-control.js +98 -0
  53. package/src/tools/agent-swarm.js +11 -2
  54. package/src/tools/agent.js +35 -1
  55. package/src/tools/diagnostics.js +175 -0
  56. package/src/tools/edit.js +25 -10
  57. package/src/tools/grep.js +62 -20
  58. package/src/tools/ignore.js +38 -14
  59. package/src/tools/index.js +10 -0
  60. package/src/tools/notebook.js +113 -0
  61. package/src/tools/read-media-file.js +10 -3
  62. package/src/tools/shell.js +34 -6
  63. package/src/tui.js +5637 -746
  64. package/src/web-public/app.js +7 -0
  65. package/src/web.js +3 -1
@@ -30,9 +30,19 @@ const PATH_ARGS = {
30
30
  Grep: ['path', 'pattern'],
31
31
  };
32
32
 
33
+ /** Strip a trailing `# …` comment. ` #` (whitespace before the hash) is the
34
+ * delimiter, so a `#` inside an argument — `Bash(echo a#b)` — is left alone. */
35
+ export function stripRuleComment(text) {
36
+ const m = /(^|\s)#/.exec(String(text == null ? '' : text));
37
+ return m ? String(text).slice(0, m.index + m[1].length).trim() : String(text == null ? '' : text).trim();
38
+ }
39
+
33
40
  // `Bash(npm run test)` / `Read(./src/**)` -> { tool, arg }
34
41
  export function parseRule(rule) {
35
- const text = String(rule == null ? '' : rule).trim();
42
+ // The comment is stripped FIRST. Without this a rule carrying `# match: …`
43
+ // examples parsed as null (the text no longer ends with `)`) and the rule
44
+ // silently stopped matching anything — so annotating a rule broke it.
45
+ const text = stripRuleComment(rule);
36
46
  if (!text) return null;
37
47
  const open = text.indexOf('(');
38
48
  if (open === -1) return { tool: text, arg: null };
@@ -118,3 +128,122 @@ export function describeRules(rules) {
118
128
  }
119
129
  return lines.join('\n');
120
130
  }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Rule self-tests
134
+ // ---------------------------------------------------------------------------
135
+ // A rule that silently matches NOTHING is worse than no rule at all: the user
136
+ // believes `npm test` is allowed, the prompt appears anyway, and nothing says why.
137
+ // A typo in a glob (`./src/*` vs `./src/**`) is invisible until the prompt shows up
138
+ // at the wrong moment.
139
+ //
140
+ // So a rule may carry its own examples, checked when the rules are read:
141
+ //
142
+ // Bash(npm run test *) # match: npm run test foo | not_match: npm run tests
143
+ //
144
+ // This mirrors codex's `prefix_rule(match=[...], not_match=[...])`, which validates
145
+ // its examples at load time. The examples live on the SAME line as the rule, as a
146
+ // trailing comment, so a rule and its test cannot drift into two places and the
147
+ // line-based config format does not have to change.
148
+ //
149
+ // Several examples are separated by `|`. An example is the ARGUMENT the rule
150
+ // describes — the command for Bash, the path for a file tool — so no quoting is
151
+ // needed for the common case.
152
+
153
+ /** Split one rule line into `{ rule, match, not_match }`. */
154
+ export function parseRuleLine(line) {
155
+ const text = String(line == null ? '' : line);
156
+ // A ` #` delimiter, so a `#` inside an argument stays part of the rule.
157
+ const delim = /(^|\s)#/.exec(text);
158
+ const ruleText = delim ? text.slice(0, delim.index + delim[1].length).trim() : text.trim();
159
+ const comment = delim ? text.slice(delim.index + delim[0].length).trim() : '';
160
+ const out = { rule: ruleText, match: [], not_match: [] };
161
+ if (!comment) return out;
162
+ // Scan for the markers rather than splitting on them: `split` with a LOOKAHEAD
163
+ // of `\bnot[_-]?match` splits `not-match` into `not-` and `match:` (the boundary
164
+ // before `match` matched first), which produced two unusable halves. An explicit
165
+ // scan takes the longest marker at each position and cannot do that.
166
+ const marker = /(not[_-]?match|match)\s*:/gi;
167
+ const found = [];
168
+ let m;
169
+ while ((m = marker.exec(comment)) !== null) {
170
+ found.push({ key: /^not/i.test(m[1]) ? 'not_match' : 'match', from: m.index, bodyStart: m.index + m[0].length });
171
+ }
172
+ for (let i = 0; i < found.length; i++) {
173
+ const end = i + 1 < found.length ? found[i + 1].from : comment.length;
174
+ const body = comment.slice(found[i].bodyStart, end);
175
+ for (const ex of body.split('|')) {
176
+ const t = ex.trim();
177
+ if (t) out[found[i].key].push(t);
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ /** An example string -> the argument object `ruleMatches` expects. */
184
+ export function exampleArgs(toolName, example) {
185
+ const ex = String(example == null ? '' : example).trim();
186
+ if (toolName === 'Bash') return { command: ex };
187
+ return { path: ex, file_path: ex, pattern: ex };
188
+ }
189
+
190
+ /**
191
+ * Check every rule's examples. Returns `{ checks, failed }`; a failed check is
192
+ * `{ rule, kind: 'match'|'not_match', example, list }`.
193
+ *
194
+ * A rule with NO examples yields no checks: the test is opt-in, so adding this to an
195
+ * existing config cannot start reporting errors for rules nobody annotated.
196
+ */
197
+ export function selfTestRules(rules) {
198
+ const list = rules || {};
199
+ const checks = [];
200
+ const failed = [];
201
+ // `deny` is included on purpose: an over-broad deny is the MOST damaging typo of
202
+ // the three, because it can lock the agent out of the work it was asked to do.
203
+ for (const key of ['allow', 'ask', 'deny']) {
204
+ const arr = Array.isArray(list[key]) ? list[key] : [];
205
+ for (const raw of arr) {
206
+ const { rule, match, not_match } = parseRuleLine(raw);
207
+ if (!rule) continue;
208
+ const parsed = parseRule(rule);
209
+ if (!parsed) {
210
+ failed.push({ rule, kind: 'parse', example: '', list: key });
211
+ continue;
212
+ }
213
+ for (const ex of match) {
214
+ const pass = ruleMatches(parsed, parsed.tool, exampleArgs(parsed.tool, ex));
215
+ checks.push({ rule, kind: 'match', example: ex, pass, list: key });
216
+ if (!pass) failed.push({ rule, kind: 'match', example: ex, list: key });
217
+ }
218
+ for (const ex of not_match) {
219
+ const pass = !ruleMatches(parsed, parsed.tool, exampleArgs(parsed.tool, ex));
220
+ checks.push({ rule, kind: 'not_match', example: ex, pass, list: key });
221
+ if (!pass) failed.push({ rule, kind: 'not_match', example: ex, list: key });
222
+ }
223
+ }
224
+ }
225
+ return { checks, failed };
226
+ }
227
+
228
+ /** The self-test block for the /permissions panel. */
229
+ export function describeSelfTest(result) {
230
+ if (!result || !result.checks.length) {
231
+ return result && result.failed.length
232
+ ? `${result.failed.length} rule(s) could not be parsed:\n`
233
+ + result.failed.map((f) => ` FAIL ${f.list}: ${f.rule}`).join('\n')
234
+ : 'No rule has examples yet. Add them on the rule line:\n'
235
+ + ' Bash(git push *) # match: git push | not_match: git pull';
236
+ }
237
+ const out = [`${result.checks.length} example(s) checked, ${result.failed.length} failed:`];
238
+ for (const c of result.checks) {
239
+ const want = c.kind === 'match' ? 'should match ' : 'should NOT match';
240
+ out.push(` ${c.pass ? 'ok ' : 'FAIL'} ${want} ${c.list}: ${c.rule} <- ${c.example}`);
241
+ }
242
+ return out.join('\n');
243
+ }
244
+
245
+ export default {
246
+ parseRule, ruleMatches, decideFromRules, describeRules,
247
+ parseRuleLine, selfTestRules, describeSelfTest, exampleArgs,
248
+ };
249
+
package/src/plan.js CHANGED
@@ -1,7 +1,8 @@
1
- // Plan review helpers — turning a model's <plan> block into something the user
1
+ // Plan review helpers — turning a model's plan block into something the user
2
2
  // can actually review before any file is touched.
3
3
  //
4
- // Plan mode already asks the model for a <plan>…</plan> block and prompts to
4
+ // Plan mode already asks the model for a <|plan|>…</|plan|> block and prompts to
5
+ // approve it. What this module adds is the review material and the persistence:
5
6
  // approve it. What this module adds is the review material and the persistence:
6
7
  //
7
8
  // * filesReferenced(plan) — which paths the plan says it will touch, so the