wave-agent-sdk 1.1.4 → 1.2.0

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 (89) hide show
  1. package/dist/agent.d.ts +72 -24
  2. package/dist/agent.js +123 -31
  3. package/dist/builtin/plugins.js +17 -2
  4. package/dist/builtin/skills/settings.js +6 -8
  5. package/dist/core/plugin.d.ts +4 -0
  6. package/dist/core/plugin.js +7 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/managers/aiManager.d.ts +0 -24
  10. package/dist/managers/aiManager.js +23 -181
  11. package/dist/managers/bashModeManager.d.ts +33 -0
  12. package/dist/managers/bashModeManager.js +110 -0
  13. package/dist/managers/hookManager.d.ts +5 -0
  14. package/dist/managers/hookManager.js +7 -0
  15. package/dist/managers/liveConfigManager.js +3 -3
  16. package/dist/managers/mcpManager.d.ts +23 -0
  17. package/dist/managers/mcpManager.js +162 -14
  18. package/dist/managers/messageManager.d.ts +12 -13
  19. package/dist/managers/messageManager.js +63 -60
  20. package/dist/managers/permissionManager.d.ts +29 -0
  21. package/dist/managers/permissionManager.js +158 -70
  22. package/dist/managers/planManager.d.ts +9 -0
  23. package/dist/managers/planManager.js +19 -1
  24. package/dist/managers/skillManager.d.ts +31 -0
  25. package/dist/managers/skillManager.js +122 -12
  26. package/dist/managers/slashCommandManager.js +9 -28
  27. package/dist/managers/subagentManager.d.ts +7 -0
  28. package/dist/managers/subagentManager.js +61 -9
  29. package/dist/managers/workflowManager.js +6 -0
  30. package/dist/prompts/index.d.ts +0 -1
  31. package/dist/prompts/index.js +0 -4
  32. package/dist/services/MarketplaceService.js +36 -14
  33. package/dist/services/configurationService.d.ts +34 -2
  34. package/dist/services/configurationService.js +123 -16
  35. package/dist/services/initializationService.js +2 -2
  36. package/dist/services/jsonlHandler.d.ts +14 -0
  37. package/dist/services/jsonlHandler.js +44 -1
  38. package/dist/services/memory.d.ts +14 -0
  39. package/dist/services/memory.js +33 -0
  40. package/dist/services/officialMarketplaceMirror.d.ts +85 -0
  41. package/dist/services/officialMarketplaceMirror.js +289 -0
  42. package/dist/services/remoteSettingsService.js +4 -4
  43. package/dist/services/session.js +30 -13
  44. package/dist/services/worktreeHooks.js +6 -1
  45. package/dist/stdio/index.d.ts +10 -0
  46. package/dist/stdio/index.js +10 -0
  47. package/dist/stdio/notificationRouter.d.ts +38 -0
  48. package/dist/stdio/notificationRouter.js +96 -0
  49. package/dist/stdio/rpcClient.d.ts +18 -0
  50. package/dist/stdio/rpcClient.js +10 -0
  51. package/dist/stdio/stdioAgent.d.ts +222 -0
  52. package/dist/stdio/stdioAgent.js +341 -0
  53. package/dist/tools/bashTool.js +2 -0
  54. package/dist/tools/exitPlanMode.js +10 -2
  55. package/dist/types/agent.d.ts +8 -0
  56. package/dist/types/commands.d.ts +7 -0
  57. package/dist/types/configuration.d.ts +6 -1
  58. package/dist/types/hooks.d.ts +1 -0
  59. package/dist/types/hooks.js +19 -0
  60. package/dist/types/mcp.d.ts +3 -0
  61. package/dist/types/messaging.d.ts +1 -8
  62. package/dist/types/skills.d.ts +11 -0
  63. package/dist/utils/bashParser.d.ts +17 -0
  64. package/dist/utils/bashParser.js +72 -0
  65. package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
  66. package/dist/utils/bashStructure/bashLexer.js +676 -0
  67. package/dist/utils/bashStructure/bashParser.d.ts +144 -0
  68. package/dist/utils/bashStructure/bashParser.js +606 -0
  69. package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
  70. package/dist/utils/bashStructure/bashSemantics.js +477 -0
  71. package/dist/utils/bashStructure/index.d.ts +26 -0
  72. package/dist/utils/bashStructure/index.js +27 -0
  73. package/dist/utils/bashStructure/types.d.ts +62 -0
  74. package/dist/utils/bashStructure/types.js +47 -0
  75. package/dist/utils/container.d.ts +6 -0
  76. package/dist/utils/container.js +9 -0
  77. package/dist/utils/containerSetup.d.ts +11 -1
  78. package/dist/utils/containerSetup.js +26 -11
  79. package/dist/utils/fileUtils.d.ts +11 -0
  80. package/dist/utils/fileUtils.js +37 -0
  81. package/dist/utils/messageOperations.d.ts +0 -18
  82. package/dist/utils/messageOperations.js +0 -62
  83. package/dist/utils/subagentParser.js +9 -2
  84. package/dist/utils/tokenCalculation.js +0 -8
  85. package/dist/utils/worktreeUtils.d.ts +2 -1
  86. package/dist/utils/worktreeUtils.js +64 -34
  87. package/package.json +6 -1
  88. package/dist/managers/bangManager.d.ts +0 -26
  89. package/dist/managers/bangManager.js +0 -78
@@ -0,0 +1,606 @@
1
+ /**
2
+ * Recursive-descent parser for the structure-aware bash parser.
3
+ *
4
+ * Consumes the token stream from `bashLexer.ts` and produces a light AST that
5
+ * preserves shell structure: command lists/and-or chains, pipelines,
6
+ * subshells/brace groups, for/while/until/if/case loops and function
7
+ * definitions. Each structural token (do/done/then/fi/esac…) is consumed by
8
+ * the parser and never reaches the semantic layer as a "command".
9
+ *
10
+ * The grammar handled here is deliberately the subset bash uses in agent
11
+ * tool commands:
12
+ * program := list (sep list)* sep ∈ {newline, ';', eof}
13
+ * list := andor (op andor)* op ∈ {&& || | |& &}
14
+ * andor := command
15
+ * command := simple | subshell | brace-group | for | while | until
16
+ * | if | case | function | select
17
+ *
18
+ * Anything not modelled (heredocs, arithmetic expansion, brace expansion,
19
+ * process substitution, `${x:-y}`-style parameter expansion, unterminated
20
+ * constructs) is a FAILURE: `parseProgram` returns null and the caller treats
21
+ * it as a fail-closed "unsupported" result (ask instead of auto-allowing).
22
+ */
23
+ import { isRedirectWord, redirectOpKind, } from "./bashLexer.js";
24
+ /** Reserved words — structural only when they open a command position. */
25
+ const STRUCTURAL_KEYWORDS = new Set([
26
+ "if",
27
+ "then",
28
+ "elif",
29
+ "else",
30
+ "fi",
31
+ "for",
32
+ "select",
33
+ "while",
34
+ "until",
35
+ "do",
36
+ "done",
37
+ "case",
38
+ "esac",
39
+ "function",
40
+ "in",
41
+ "{",
42
+ "}",
43
+ "!",
44
+ "time",
45
+ ]);
46
+ const MAX_DEPTH = 40;
47
+ export class BashParser {
48
+ constructor(tokens) {
49
+ this.pos = 0;
50
+ this.depth = 0;
51
+ this.tokens = tokens;
52
+ }
53
+ /** Parse the full token stream. */
54
+ parseProgram() {
55
+ const program = this.parseSequence(new Set());
56
+ if (program === null)
57
+ return null;
58
+ // Anything left over that isn't trailing newline / eof → malformed.
59
+ const t = this.peek();
60
+ if (t.t === "eof" || t.t === "nl")
61
+ return program;
62
+ return null;
63
+ }
64
+ peek() {
65
+ return this.tokens[this.pos] ?? { t: "eof" };
66
+ }
67
+ next() {
68
+ const t = this.peek();
69
+ if (this.pos < this.tokens.length)
70
+ this.pos++;
71
+ return t;
72
+ }
73
+ isOp(op) {
74
+ const t = this.peek();
75
+ return t.t === "op" && t.op === op;
76
+ }
77
+ /** True when the current word is a bare (unquoted) keyword. */
78
+ isBareWord(raw) {
79
+ const t = this.peek();
80
+ return (t.t === "word" &&
81
+ t.raw === raw &&
82
+ t.parts.length === 1 &&
83
+ t.parts[0]?.t === "lit" &&
84
+ t.parts[0]?.q === "none");
85
+ }
86
+ /** Skip newline tokens. */
87
+ skipNl() {
88
+ while (this.peek().t === "nl")
89
+ this.next();
90
+ }
91
+ /**
92
+ * Parse statements until a terminator in `stop` (as bare keyword) or a
93
+ * closing op. Used for compound bodies (do…done, then…fi, case arms).
94
+ */
95
+ parseSequence(stop) {
96
+ if (this.depth > MAX_DEPTH)
97
+ return null;
98
+ this.depth++;
99
+ try {
100
+ const stmts = [];
101
+ for (;;) {
102
+ this.skipNl();
103
+ const t = this.peek();
104
+ if (t.t === "eof")
105
+ break;
106
+ if (t.t === "op") {
107
+ if (t.op === ")" ||
108
+ t.op === ";;" ||
109
+ t.op === ";&" ||
110
+ t.op === ";;&") {
111
+ break;
112
+ }
113
+ if (t.op === ";") {
114
+ this.next();
115
+ continue;
116
+ }
117
+ if (t.op === "&") {
118
+ // stray background op — treat as separator
119
+ this.next();
120
+ continue;
121
+ }
122
+ return null; // '(' or others not valid here
123
+ }
124
+ if (t.t === "word") {
125
+ if (STRUCTURAL_KEYWORDS.has(t.raw) && this.isBareWord(t.raw)) {
126
+ // Closing keywords (then/do/else/fi/done/esac/}) only appear at
127
+ // clause boundaries: a matching one ends this sequence, a stray
128
+ // one is a syntax error (fail closed).
129
+ if (stop.has(t.raw))
130
+ break;
131
+ if (t.raw === "}")
132
+ break;
133
+ if (t.raw === "then" ||
134
+ t.raw === "do" ||
135
+ t.raw === "else" ||
136
+ t.raw === "elif" ||
137
+ t.raw === "fi" ||
138
+ t.raw === "done" ||
139
+ t.raw === "esac") {
140
+ return null;
141
+ }
142
+ // Opening keywords (if/for/while/until/case/select/function/{)
143
+ // start a nested compound — let parseChain dispatch them.
144
+ }
145
+ }
146
+ const stmt = this.parseChain();
147
+ if (stmt === null)
148
+ return null;
149
+ stmts.push(stmt);
150
+ }
151
+ return stmts;
152
+ }
153
+ finally {
154
+ this.depth--;
155
+ }
156
+ }
157
+ /** Parse an and-or chain: cmd (&&|… cmd)* */
158
+ parseChain() {
159
+ const steps = [];
160
+ let first = this.parseCommand();
161
+ if (first === null)
162
+ return null;
163
+ steps.push({ nodes: first, forkAfter: false });
164
+ for (;;) {
165
+ const t = this.peek();
166
+ if (t.t !== "op")
167
+ break;
168
+ if (t.op === "&&") {
169
+ this.next();
170
+ // && is sequential — same scope continues; only ';' etc reset lists.
171
+ const cmds = this.parseCommand();
172
+ if (cmds === null)
173
+ return null;
174
+ steps.push({ nodes: cmds, forkAfter: false });
175
+ continue;
176
+ }
177
+ if (t.op === "||" || t.op === "|" || t.op === "|&" || t.op === "&") {
178
+ this.next();
179
+ const cmds = this.parseCommand();
180
+ if (cmds === null)
181
+ return null;
182
+ steps.push({ nodes: cmds, forkAfter: true });
183
+ continue;
184
+ }
185
+ break;
186
+ }
187
+ if (steps.length === 1)
188
+ return steps[0]?.nodes[0] ?? { kind: "list", steps };
189
+ return { kind: "list", steps };
190
+ }
191
+ /** Parse a single command (simple or compound). Returns nodes array. */
192
+ parseCommand() {
193
+ if (this.depth > MAX_DEPTH)
194
+ return null;
195
+ this.depth++;
196
+ try {
197
+ this.skipNl();
198
+ // `! cmd` / `time cmd` prefixes
199
+ for (;;) {
200
+ if (this.isBareWord("!") || this.isBareWord("time")) {
201
+ this.next();
202
+ continue;
203
+ }
204
+ break;
205
+ }
206
+ const t = this.peek();
207
+ // subshell `( ... )`
208
+ if (t.t === "op" && t.op === "(") {
209
+ this.next();
210
+ const body = this.parseSequence(new Set());
211
+ if (body === null)
212
+ return null;
213
+ if (!this.isOp(")"))
214
+ return null;
215
+ this.next();
216
+ const n = { kind: "subshell", body };
217
+ return [this.absorbTrailingRedirects(n)];
218
+ }
219
+ if (t.t === "word") {
220
+ const isBare = this.isBareWord(t.raw);
221
+ if (isBare) {
222
+ switch (t.raw) {
223
+ case "if": {
224
+ this.next();
225
+ const n = this.parseIf();
226
+ return n ? [this.absorbTrailingRedirects(n)] : null;
227
+ }
228
+ case "while":
229
+ case "until": {
230
+ this.next();
231
+ const until = t.raw === "until";
232
+ const n = this.parseWhile(until);
233
+ return n ? [this.absorbTrailingRedirects(n)] : null;
234
+ }
235
+ case "for":
236
+ case "select": {
237
+ this.next();
238
+ const n = this.parseFor();
239
+ return n ? [this.absorbTrailingRedirects(n)] : null;
240
+ }
241
+ case "case": {
242
+ this.next();
243
+ const n = this.parseCase();
244
+ return n ? [this.absorbTrailingRedirects(n)] : null;
245
+ }
246
+ case "function": {
247
+ this.next();
248
+ const n = this.parseFunctionKw();
249
+ return n ? [this.absorbTrailingRedirects(n)] : null;
250
+ }
251
+ case "{": {
252
+ // brace group { list; }
253
+ this.next();
254
+ const body = this.parseSequence(new Set(["}"]));
255
+ if (body === null)
256
+ return null;
257
+ if (!this.isBareWord("}"))
258
+ return null;
259
+ this.next();
260
+ const n = { kind: "group", body };
261
+ return [this.absorbTrailingRedirects(n)];
262
+ }
263
+ default:
264
+ break;
265
+ }
266
+ }
267
+ // function definition `name() { …; }`
268
+ const func = this.tryParseFunctionDecl();
269
+ if (func)
270
+ return [this.absorbTrailingRedirects(func)];
271
+ // simple command
272
+ const words = this.parseSimpleCommandWords();
273
+ if (words === null)
274
+ return null;
275
+ return [{ kind: "cmd", words }];
276
+ }
277
+ if (t.t === "nl")
278
+ return [];
279
+ return null;
280
+ }
281
+ finally {
282
+ this.depth--;
283
+ }
284
+ }
285
+ /**
286
+ * Absorb redirections that trail a compound command (e.g. the `< input.txt`
287
+ * in `while read x; do …; done < input.txt`). Shell grammar binds them to
288
+ * the whole compound; we wrap it in a RedirectsNode so the semantic layer can
289
+ * attach the redirect text to the compound's last leaf command.
290
+ */
291
+ absorbTrailingRedirects(node) {
292
+ this.skipNl();
293
+ const redirects = [];
294
+ for (;;) {
295
+ this.skipNl();
296
+ const t = this.peek();
297
+ if (t.t !== "word" || !isRedirectWord(t))
298
+ break;
299
+ this.next();
300
+ redirects.push(t);
301
+ // Unfolded operators (`>`, `2>`, `>>`, `&>`) take the next word as their
302
+ // target; folded dups (`2>&1`) already contain everything.
303
+ if (redirectOpKind(t.raw) === "plain") {
304
+ const target = this.peek();
305
+ if (target.t !== "word" || isRedirectWord(target))
306
+ break;
307
+ redirects.push(target);
308
+ this.next();
309
+ }
310
+ }
311
+ if (redirects.length === 0)
312
+ return node;
313
+ return { kind: "redirects", target: node, redirects };
314
+ }
315
+ parseIf() {
316
+ // after `if`
317
+ const clauses = [];
318
+ let elseBody = null;
319
+ const cond = this.parseSequence(new Set(["then", "elif", "else", "fi"]));
320
+ if (cond === null)
321
+ return null;
322
+ if (!this.isBareWord("then"))
323
+ return null;
324
+ this.next();
325
+ const body = this.parseSequence(new Set(["elif", "else", "fi"]));
326
+ if (body === null)
327
+ return null;
328
+ clauses.push({ cond, body });
329
+ for (;;) {
330
+ if (this.isBareWord("elif")) {
331
+ this.next();
332
+ const c2 = this.parseSequence(new Set(["then"]));
333
+ if (c2 === null)
334
+ return null;
335
+ if (!this.isBareWord("then"))
336
+ return null;
337
+ this.next();
338
+ const b2 = this.parseSequence(new Set(["elif", "else", "fi"]));
339
+ if (b2 === null)
340
+ return null;
341
+ clauses.push({ cond: c2, body: b2 });
342
+ continue;
343
+ }
344
+ if (this.isBareWord("else")) {
345
+ this.next();
346
+ const eb = this.parseSequence(new Set(["fi"]));
347
+ if (eb === null)
348
+ return null;
349
+ elseBody = eb;
350
+ continue;
351
+ }
352
+ if (this.isBareWord("fi")) {
353
+ this.next();
354
+ return { kind: "if", clauses, elseBody };
355
+ }
356
+ return null;
357
+ }
358
+ }
359
+ parseWhile(until) {
360
+ // after while/until
361
+ const cond = this.parseSequence(new Set(["do", "done"]));
362
+ if (cond === null)
363
+ return null;
364
+ if (!this.isBareWord("do"))
365
+ return null;
366
+ this.next();
367
+ const body = this.parseSequence(new Set(["done"]));
368
+ if (body === null)
369
+ return null;
370
+ if (!this.isBareWord("done"))
371
+ return null;
372
+ this.next();
373
+ return { kind: "while", until, cond, body };
374
+ }
375
+ parseFor() {
376
+ // after for/select
377
+ const nameTok = this.peek();
378
+ if (nameTok.t !== "word")
379
+ return null;
380
+ if (!(nameTok.parts.length === 1 && nameTok.parts[0]?.t === "lit")) {
381
+ return null;
382
+ }
383
+ const varName = nameTok.raw;
384
+ this.next();
385
+ let iter = [];
386
+ // C-style for ((…)) unsupported
387
+ if (this.isOp("("))
388
+ return null;
389
+ if (this.isBareWord("in")) {
390
+ this.next();
391
+ // read words until ';' / newline / do
392
+ for (;;) {
393
+ this.skipNl();
394
+ const t = this.peek();
395
+ if (t.t === "eof")
396
+ return null;
397
+ if (t.t === "op") {
398
+ if (t.op === ";" || t.op === "(" || t.op === ")") {
399
+ if (t.op === ";" || t.op === ")") {
400
+ if (t.op === ";")
401
+ this.next();
402
+ break;
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+ if (t.t === "word") {
408
+ if (this.isBareWord("do"))
409
+ break;
410
+ iter.push(t);
411
+ this.next();
412
+ continue;
413
+ }
414
+ return null;
415
+ }
416
+ }
417
+ else {
418
+ // for name; do … done → implicit "$@"
419
+ if (this.isOp(";"))
420
+ this.next();
421
+ }
422
+ this.skipNl();
423
+ if (!this.isBareWord("do"))
424
+ return null;
425
+ this.next();
426
+ const body = this.parseSequence(new Set(["done"]));
427
+ if (body === null)
428
+ return null;
429
+ if (!this.isBareWord("done"))
430
+ return null;
431
+ this.next();
432
+ return { kind: "for", varName, iter, body };
433
+ }
434
+ parseCase() {
435
+ // after `case`
436
+ const wordTok = this.peek();
437
+ if (wordTok.t !== "word")
438
+ return null;
439
+ this.next();
440
+ if (!this.isBareWord("in"))
441
+ return null;
442
+ this.next();
443
+ const arms = [];
444
+ for (;;) {
445
+ this.skipNl();
446
+ if (this.isBareWord("esac")) {
447
+ this.next();
448
+ return { kind: "case", word: wordTok, arms };
449
+ }
450
+ // pattern list terminated by `)` — patterns may be `a|b)`.
451
+ const patterns = [];
452
+ for (;;) {
453
+ this.skipNl();
454
+ const t = this.peek();
455
+ if (t.t !== "word")
456
+ return null;
457
+ patterns.push(t);
458
+ this.next();
459
+ this.skipNl();
460
+ const n = this.peek();
461
+ if (n.t === "op" && n.op === "|") {
462
+ this.next();
463
+ continue;
464
+ }
465
+ if (n.t === "op" && n.op === ")") {
466
+ this.next();
467
+ break;
468
+ }
469
+ return null;
470
+ }
471
+ const body = this.parseSequence(new Set(["esac"]));
472
+ // body ends at ;; / ;& / ;;& / esac (parseSequence stops on op ;; etc.)
473
+ const after = this.peek();
474
+ if (after.t === "op") {
475
+ if (after.op === ";;" || after.op === ";&" || after.op === ";;&") {
476
+ this.next();
477
+ arms.push({ patterns, body: body ?? [] });
478
+ continue;
479
+ }
480
+ }
481
+ if (this.isBareWord("esac")) {
482
+ this.next();
483
+ arms.push({ patterns, body: body ?? [] });
484
+ return { kind: "case", word: wordTok, arms };
485
+ }
486
+ if (body === null)
487
+ return null;
488
+ arms.push({ patterns, body });
489
+ }
490
+ }
491
+ parseFunctionKw() {
492
+ // after `function`
493
+ const nameTok = this.peek();
494
+ if (nameTok.t !== "word")
495
+ return null;
496
+ const name = nameTok.raw;
497
+ this.next();
498
+ // optional `()` after name in `function f ()`
499
+ if (this.isOp("(")) {
500
+ this.next();
501
+ if (!this.isOp(")"))
502
+ return null;
503
+ this.next();
504
+ }
505
+ const body = this.parseBraceBody();
506
+ if (body === null)
507
+ return null;
508
+ return { kind: "function", name, body };
509
+ }
510
+ /** Detect `name() { …; }` form. Must be called at a simple-command word. */
511
+ tryParseFunctionDecl() {
512
+ const t = this.peek();
513
+ if (t.t !== "word")
514
+ return null;
515
+ // A function name cannot look like an assignment (VAR=…)
516
+ if (/^[A-Za-z_][A-Za-z0-9_]*=[^=]/.test(t.raw))
517
+ return null;
518
+ // Lookahead: word '(' ')'
519
+ const afterName = this.tokens[this.pos + 1];
520
+ if (afterName?.t !== "op" || afterName.op !== "(")
521
+ return null;
522
+ // Confirm matching ')' after optional whitespace (no whitespace tokens in
523
+ // our stream — spaces are skipped), so next op must be ')' or word '(').
524
+ let j = this.pos + 2;
525
+ while (this.tokens[j]?.t === "nl")
526
+ j++;
527
+ const afterParen = this.tokens[j];
528
+ if (afterParen?.t === "op" && afterParen.op === ")") {
529
+ this.next(); // name
530
+ this.next(); // (
531
+ this.next(); // )
532
+ const body = this.parseBraceBody();
533
+ if (body === null)
534
+ return null;
535
+ return { kind: "function", name: t.raw, body };
536
+ }
537
+ return null;
538
+ }
539
+ /** Parse `{ …; }` compound body for function definitions. */
540
+ parseBraceBody() {
541
+ this.skipNl();
542
+ if (!this.isBareWord("{")) {
543
+ // function body may be a single command without braces (rare) — allow
544
+ const single = this.parseChain();
545
+ return single ? [single] : null;
546
+ }
547
+ this.next();
548
+ const body = this.parseSequence(new Set(["}"]));
549
+ if (body === null)
550
+ return null;
551
+ if (!this.isBareWord("}"))
552
+ return null;
553
+ this.next();
554
+ return body;
555
+ }
556
+ /** Collect words of a simple command until a chain separator. */
557
+ parseSimpleCommandWords() {
558
+ const words = [];
559
+ for (;;) {
560
+ const t = this.peek();
561
+ if (t.t === "eof")
562
+ break;
563
+ if (t.t === "nl")
564
+ break;
565
+ if (t.t === "op") {
566
+ if (t.op === ";" ||
567
+ t.op === "&&" ||
568
+ t.op === "||" ||
569
+ t.op === "|" ||
570
+ t.op === "|&" ||
571
+ t.op === "&" ||
572
+ t.op === ")") {
573
+ break;
574
+ }
575
+ // ( or { appearing mid-command → malformed simple command
576
+ if (t.op === "(")
577
+ return null;
578
+ break;
579
+ }
580
+ if (t.t === "word") {
581
+ if (STRUCTURAL_KEYWORDS.has(t.raw) &&
582
+ this.isBareWord(t.raw) &&
583
+ words.length === 0) {
584
+ // structural keyword at command start is handled in parseCommand;
585
+ // reaching here means parseCommand already dispatched. Guard anyway.
586
+ return null;
587
+ }
588
+ words.push(t);
589
+ this.next();
590
+ continue;
591
+ }
592
+ return null;
593
+ }
594
+ return words.length > 0 ? words : null;
595
+ }
596
+ }
597
+ /**
598
+ * Parse a token stream into a program. Returns null on any construct that
599
+ * cannot be statically modelled (fail-closed → caller asks).
600
+ */
601
+ export function parseTokens(tokens) {
602
+ if (tokens === null)
603
+ return null;
604
+ const parser = new BashParser(tokens);
605
+ return parser.parseProgram();
606
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Semantic layer for the structure-aware bash parser.
3
+ *
4
+ * Walks the parse tree from `bashParser.ts` and expands every executable unit
5
+ * into a flat list of "leaf" simple commands (BashLeaf). The permission layer
6
+ * (P2) classifies each leaf with the exact rules already used for plain
7
+ * commands (read-only sets, path Safe Zone checks, allow rules).
8
+ *
9
+ * Semantics mirrors Claude Code's collectCommands / resolveSimpleExpansion:
10
+ *
11
+ * - Compound commands contribute the leaves of *every* branch/iteration body:
12
+ * statically we cannot know which branch runs, so each is checked; an
13
+ * unsafe leaf anywhere forces the whole command to ask.
14
+ * - `$(...)` / backticks are expanded recursively into their own leaves. A
15
+ * substitution that appears inside one of a leaf's own argument words marks
16
+ * that leaf `unsafe` (`command-substitution`) — its expansion is dynamic.
17
+ * - Assignments are tracked in a scope so later leaves in the same sequential
18
+ * chain can resolve bare references (spec: `x=abc; echo $x` — the value is a
19
+ * statically known literal). Anything not literally known stays UNKNOWN.
20
+ * - A bare (unquoted) reference to an UNKNOWN variable marks the leaf unsafe
21
+ * (`bare-variable`): word-splitting/globbing could inject extra arguments.
22
+ * Quoted `"$f"` references are always safe (a single word). `$@`/`$*` are
23
+ * never safe bare *or* quoted — they expand to any number of words.
24
+ * - Arithmetic/brace/process substitutions, heredocs, eval/trap and any parse
25
+ * failure fail closed: the whole parse reports `unsupported` (ask).
26
+ *
27
+ * Scope rules mirror bash execution conservatively:
28
+ * - Sequential `;` / `&&` steps share a scope (assignments propagate).
29
+ * - Fork steps (`||`, `|`, `|&`, `&`) and compound bodies run from a copy, so
30
+ * an assignment on one side is never assumed on the other.
31
+ * - Assignment-prefixed commands (`VAR=x cmd`) keep the *incoming* scope for
32
+ * their own argument expansion (bash expands args before applying prefix).
33
+ */
34
+ import { type BashStructureResult } from "./types.js";
35
+ export declare class BashSemantics {
36
+ private leaves;
37
+ /** Parse and expand a full command string. */
38
+ static analyze(command: string): BashStructureResult;
39
+ private run;
40
+ /** Best-effort reason classification when the lexer rejects the input. */
41
+ private unsupportedFor;
42
+ private walkProgram;
43
+ private walkStmt;
44
+ /** Bind compound-level redirects to the last leaf produced by the target. */
45
+ private mergeRedirectsIntoLastLeaf;
46
+ private walkCmd;
47
+ private walkFor;
48
+ private walkWhile;
49
+ private walkIf;
50
+ private walkCase;
51
+ /**
52
+ * Build a BashLeaf from a command's words. Emits leaves for command
53
+ * substitutions embedded in argument words. Throws Unsupported on
54
+ * fail-closed constructs.
55
+ */
56
+ private buildLeaf;
57
+ /**
58
+ * Classify one word's parts for structural safety. Pushes reasons onto
59
+ * `reasons` for unsafe leaf conditions. Throws Unsupported for fail-closed
60
+ * construct kinds.
61
+ */
62
+ private classifyWordParts;
63
+ private applyAssignment;
64
+ /** Collect leaves of command substitutions in a word (iter/case positions). */
65
+ private collectWordCmdSubs;
66
+ /** Recursively parse a $() body and append its leaves. */
67
+ private emitSubstitutionLeaves;
68
+ /** Mark variables captured by `read` in a condition list as unknown. */
69
+ private collectReadCaptures;
70
+ }