@yagni-app/code 1.0.3 → 1.0.5

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.
@@ -309,14 +309,31 @@ class QuestionComponent {
309
309
  const indexText = `${this.otherIndex + 1}.`.padEnd(indexPad);
310
310
  if (isSelected) {
311
311
  // Inline input: show current text + inverse-video cursor at end.
312
+ // Wraps like a growing text field (the prompt input), never truncated.
312
313
  const cursor = "\x1b[7m \x1b[27m"; // space with reverse video = visible cursor
314
+ const gutter = `${marker} ${indexText}`;
315
+ const gutterWidth = visibleWidth(gutter);
316
+ // Reserve one column so the trailing cursor always fits on the last line.
317
+ const wrapWidth = Math.max(1, width - gutterWidth - 1);
313
318
  const text = this.otherValue || " ";
314
- lines.push(clampLine(`${marker} ${indexText}${text}${cursor}`, width));
319
+ const wrapped = wrapTextWithAnsi(text, wrapWidth);
320
+ wrapped.forEach((w, i) => {
321
+ const prefix = i === 0 ? gutter : " ".repeat(gutterWidth);
322
+ const body = i === wrapped.length - 1 ? `${w}${cursor}` : w;
323
+ lines.push(clampLine(prefix + body, width));
324
+ });
315
325
  }
316
326
  else {
317
327
  // Not focused: show placeholder or current text in dim.
318
328
  const display = this.otherValue || "(type a custom answer)";
319
- lines.push(clampLine(`${marker} ${indexText}${t.fg("dim", display)}`, width));
329
+ const gutter = `${marker} ${indexText}`;
330
+ const gutterWidth = visibleWidth(gutter);
331
+ const wrapWidth = Math.max(1, width - gutterWidth);
332
+ const wrapped = wrapTextWithAnsi(t.fg("dim", display), wrapWidth);
333
+ wrapped.forEach((w, i) => {
334
+ const prefix = i === 0 ? gutter : " ".repeat(gutterWidth);
335
+ lines.push(clampLine(prefix + w, width));
336
+ });
320
337
  }
321
338
  }
322
339
  handleInput(data) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Renders three lines:
5
5
  * 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
6
- * 2. model · ↑in ↓out $cost · ctx% (session stats; context % is an integer)
6
+ * 2. ↑in ↓out $cost · ctx% (session stats; context % is an integer)
7
7
  * 3. extension statuses (branding, todo counter, mode) joined by " · "
8
8
  *
9
9
  * --- How to customize the status bar (for future tickets) ---
@@ -24,11 +24,11 @@
24
24
  * - `footerData` — ReadonlyFooterDataProvider: `getGitBranch()`,
25
25
  * `getExtensionStatuses()` (statuses set via `ctx.ui.setStatus(key, text)`),
26
26
  * `getAvailableProviderCount()`
27
- * Model info, token stats, and context usage are NOT on `footerData` — they're
28
- * on `ctx` (the ExtensionContext passed to the session_start handler):
29
- * `ctx.model`, `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them
30
- * through a closure if the footer needs them (this module's
31
- * `createYagniFooterFactory` does exactly that).
27
+ * Token stats and context usage are NOT on `footerData` — they're on `ctx`
28
+ * (the ExtensionContext passed to the session_start handler):
29
+ * `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them through a closure
30
+ * if the footer needs them (this module's `createYagniFooterFactory` does
31
+ * exactly that).
32
32
  *
33
33
  * COMPONENT CONTRACT: the returned object needs `render(width: number): string[]`
34
34
  * (returns the lines to display, one string per row) and `invalidate()` (called
@@ -94,8 +94,7 @@ export declare function detectGitInfo(cwd: string, home: string | undefined): Gi
94
94
  /** Pure line-builder, exported for tests. All data injected; colors via theme. */
95
95
  export declare function renderFooterLines(input: {
96
96
  git: GitInfo;
97
- model: string;
98
- /** Current permission mode; shown on line 2 to the left of the model as "<mode> mode". */
97
+ /** Current permission mode; shown on line 2 to the left of the stats as "<mode> mode". */
99
98
  mode?: PermissionMode | null;
100
99
  /** Formatted `+N -M` / `±`, or null when clean — appended to the branch as `[ … ]`. */
101
100
  diff?: string | null;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Renders three lines:
5
5
  * 1. folder · [worktree] · branch (git context; ~-path fallback off-repo)
6
- * 2. model · ↑in ↓out $cost · ctx% (session stats; context % is an integer)
6
+ * 2. ↑in ↓out $cost · ctx% (session stats; context % is an integer)
7
7
  * 3. extension statuses (branding, todo counter, mode) joined by " · "
8
8
  *
9
9
  * --- How to customize the status bar (for future tickets) ---
@@ -24,11 +24,11 @@
24
24
  * - `footerData` — ReadonlyFooterDataProvider: `getGitBranch()`,
25
25
  * `getExtensionStatuses()` (statuses set via `ctx.ui.setStatus(key, text)`),
26
26
  * `getAvailableProviderCount()`
27
- * Model info, token stats, and context usage are NOT on `footerData` — they're
28
- * on `ctx` (the ExtensionContext passed to the session_start handler):
29
- * `ctx.model`, `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them
30
- * through a closure if the footer needs them (this module's
31
- * `createYagniFooterFactory` does exactly that).
27
+ * Token stats and context usage are NOT on `footerData` — they're on `ctx`
28
+ * (the ExtensionContext passed to the session_start handler):
29
+ * `ctx.sessionManager`, `ctx.getContextUsage()`. Thread them through a closure
30
+ * if the footer needs them (this module's `createYagniFooterFactory` does
31
+ * exactly that).
32
32
  *
33
33
  * COMPONENT CONTRACT: the returned object needs `render(width: number): string[]`
34
34
  * (returns the lines to display, one string per row) and `invalidate()` (called
@@ -244,7 +244,7 @@ export function renderFooterLines(input, theme, width, padX = 0) {
244
244
  }
245
245
  }
246
246
  const line1 = pad + truncateToWidth(line1Parts.join(sep), contentWidth, dim("…"));
247
- // Line 2: [mode ·] model · ↑in ↓out $cost · ctx%
247
+ // Line 2: [mode ·] ↑in ↓out $cost · ctx%
248
248
  // Child-process spend (subagents, advisor, /go) joins the driver totals so
249
249
  // the footer matches the per-child receipts and /cost's session scope.
250
250
  const child = input.childUsage;
@@ -265,7 +265,6 @@ export function renderFooterLines(input, theme, width, padX = 0) {
265
265
  const modeLabel = modeDisplay(input.mode);
266
266
  line2Parts.push(theme.fg(modeLabel.color, modeLabel.text) + dim(" (shift+tab to change)"));
267
267
  }
268
- line2Parts.push(dim(input.model));
269
268
  if (stats)
270
269
  line2Parts.push(dim(stats));
271
270
  line2Parts.push(theme.fg(contextColor(input.contextPercent), percentText));
@@ -307,7 +306,6 @@ export function createYagniFooterFactory(ctx, modeHolder, invalidateHandle, chil
307
306
  .map(([, text]) => text);
308
307
  return renderFooterLines({
309
308
  git: gitInfo(),
310
- model: ctx.model?.id ?? "no-model",
311
309
  mode: modeHolder?.get() ?? null,
312
310
  diff: formatDiffStat(diffStatCache?.get() ?? null),
313
311
  usage: collectUsage(ctx.sessionManager),
@@ -885,9 +885,12 @@ export async function registerYagni(pi, deps = {}) {
885
885
  // Surface Pi's native find/grep/ls on the driver. These are Pi's
886
886
  // equivalents of Claude Code's Glob/Grep/LS; the /go stages and subagents
887
887
  // already pass them explicitly, but the driver defaults to read/bash/edit/write
888
- // and otherwise reaches for `bash` find/grep/ls.
888
+ // and otherwise reaches for `bash` find/grep/ls. Dedupe against the active
889
+ // set — condensedTools re-registers these built-ins, so they can already be
890
+ // active here, and a duplicated name in the request's tool list is a hard
891
+ // 400 on some providers (Baseten: "Tool names must be unique.").
889
892
  try {
890
- pi.setActiveTools([...pi.getActiveTools(), "grep", "find", "ls"]);
893
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), "grep", "find", "ls"])]);
891
894
  }
892
895
  catch {
893
896
  // Tool enrichment must never break session start.
@@ -616,10 +616,20 @@ function classifySegmentTokens(rawTokens, policy, opts) {
616
616
  }
617
617
  const { tokens: strippedTokens, stripped } = stripLeadingTokens(rawTokens);
618
618
  if (strippedTokens.length === 0) {
619
+ // A segment made only of environment assignments (`FOO=bar`) is a no-op: it
620
+ // sets shell variables and executes nothing. Recognize it so a compound like
621
+ // `SF="$F"; echo hi` is not floored by its leading assignment. Safety holds
622
+ // because classifyCommand's construct floor + substitution danger scan run
623
+ // afterward: an assignment whose value is a `$(...)` substitution frees the
624
+ // inner text for the danger scan and downgrades the overall result to prompt
625
+ // or forbidden there.
626
+ if (!opts.forbiddenOnly && rawTokens.length > 0 && rawTokens.every((t) => ENV_ASSIGNMENT_RE.test(t))) {
627
+ return { decision: "allow", justification: "no-op environment assignment" };
628
+ }
619
629
  // env standalone: `env` or `env VAR=val` with no following command prints
620
630
  // environment variables — a read-only operation. stripLeadingTokens removes
621
631
  // `env` as a wrapper word, leaving empty tokens. Recognize this case instead
622
- // of returning "empty command segment" (YAG-549).
632
+ // of returning "empty command segment".
623
633
  if (!opts.forbiddenOnly && rawTokens.length > 0 && basenameToken(rawTokens[0]) === "env") {
624
634
  const onlyEnvAndAssignments = rawTokens.every((t) => basenameToken(t) === "env" || ENV_ASSIGNMENT_RE.test(t));
625
635
  if (onlyEnvAndAssignments) {
@@ -951,6 +961,9 @@ export const DEFAULT_EXEC_POLICY = {
951
961
  { pattern: ["printf"], decision: "allow", justification: "print formatted text" },
952
962
  { pattern: ["whoami"], decision: "allow", justification: "show current user" },
953
963
  { pattern: ["uname"], decision: "allow", justification: "show system info" },
964
+ { pattern: ["ps"], decision: "allow", justification: "list processes (read-only)" },
965
+ { pattern: ["lsof"], decision: "allow", justification: "list open files and ports (read-only)" },
966
+ { pattern: ["sleep"], decision: "allow", justification: "no-op delay" },
954
967
  { pattern: ["git", "status"], decision: "allow", justification: "show working tree status" },
955
968
  { pattern: ["git", "log"], decision: "allow", justification: "show commit log" },
956
969
  { pattern: ["git", "diff"], decision: "allow", justification: "show changes" },
@@ -989,6 +1002,9 @@ export const DEFAULT_EXEC_POLICY = {
989
1002
  },
990
1003
  { pattern: ["linear", ["--help", "-h"]], decision: "allow", justification: "show Linear CLI help" },
991
1004
  { pattern: ["linear", ["--version", "-V"]], decision: "allow", justification: "show Linear CLI version" },
1005
+ { pattern: ["linear", "user", "list"], decision: "allow", justification: "list Linear workspace members (read-only)" },
1006
+ { pattern: ["linear", "team", ["list", "members", "states", "id"]], decision: "allow", justification: "read Linear team data (read-only)" },
1007
+ { pattern: ["linear", "project", ["list", "view"]], decision: "allow", justification: "read Linear project data (read-only)" },
992
1008
  // gh api — REST GET (no params, no method override) and GraphQL queries (no mutation)
993
1009
  // gh api defaults to POST when -f/-F params are present, so params are disqualifying.
994
1010
  // GraphQL is always POST, but queries are reads; mutations/subscriptions are disqualifying.
@@ -1065,7 +1081,6 @@ export const DEFAULT_EXEC_POLICY = {
1065
1081
  { pattern: ["tar"], decision: "prompt", justification: "archive operation" },
1066
1082
  { pattern: ["zip"], decision: "prompt", justification: "archive operation" },
1067
1083
  { pattern: ["unzip"], decision: "prompt", justification: "archive operation" },
1068
- { pattern: ["ps"], decision: "prompt", justification: "lists processes" },
1069
1084
  { pattern: ["kill"], decision: "prompt", justification: "sends a signal to a process" },
1070
1085
  { pattern: ["chmod"], decision: "prompt", justification: "permission change — review the mode" },
1071
1086
  { pattern: ["chown"], decision: "prompt", justification: "ownership change" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -41,5 +41,5 @@
41
41
  "turndown": "^7.2.4",
42
42
  "typebox": "^1.3.15"
43
43
  },
44
- "yagniSourceSha": "e8e3f653ae3366f156535d53740b775e21fc6fb4"
44
+ "yagniSourceSha": "5bd359dcd98d8104d425f698a65ed60d780df0af"
45
45
  }