@aligndottech/cli 0.35.0 → 0.36.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 (32) hide show
  1. package/dist/commands/import/sessions.d.ts +16 -4
  2. package/dist/commands/import/sessions.d.ts.map +1 -1
  3. package/dist/commands/import/sessions.js +110 -17
  4. package/dist/commands/import/sessions.js.map +1 -1
  5. package/dist/lib/decision-render.d.ts +35 -0
  6. package/dist/lib/decision-render.d.ts.map +1 -0
  7. package/dist/lib/decision-render.js +149 -0
  8. package/dist/lib/decision-render.js.map +1 -0
  9. package/dist/lib/local-gateway-client.d.ts.map +1 -1
  10. package/dist/lib/local-gateway-client.js +4 -1
  11. package/dist/lib/local-gateway-client.js.map +1 -1
  12. package/dist/lib/local-llm.d.ts +66 -1
  13. package/dist/lib/local-llm.d.ts.map +1 -1
  14. package/dist/lib/local-llm.js +165 -11
  15. package/dist/lib/local-llm.js.map +1 -1
  16. package/dist/lib/local-relationship-classifier.d.ts +18 -0
  17. package/dist/lib/local-relationship-classifier.d.ts.map +1 -1
  18. package/dist/lib/local-relationship-classifier.js +32 -6
  19. package/dist/lib/local-relationship-classifier.js.map +1 -1
  20. package/dist/lib/sessions/confirm-freetext.d.ts +43 -0
  21. package/dist/lib/sessions/confirm-freetext.d.ts.map +1 -0
  22. package/dist/lib/sessions/confirm-freetext.js +82 -0
  23. package/dist/lib/sessions/confirm-freetext.js.map +1 -0
  24. package/dist/lib/sessions/extract-freetext.d.ts +26 -0
  25. package/dist/lib/sessions/extract-freetext.d.ts.map +1 -0
  26. package/dist/lib/sessions/extract-freetext.js +53 -0
  27. package/dist/lib/sessions/extract-freetext.js.map +1 -0
  28. package/dist/lib/token-estimate.d.ts +17 -0
  29. package/dist/lib/token-estimate.d.ts.map +1 -0
  30. package/dist/lib/token-estimate.js +24 -0
  31. package/dist/lib/token-estimate.js.map +1 -0
  32. package/package.json +10 -1
@@ -0,0 +1,82 @@
1
+ import { DETERMINISTIC_TEMPERATURE } from '@aligndottech/connector-core';
2
+ import { callChatDetailed, CLASSIFIER_MAX_TOKENS, hasConfiguredProvider, noProviderHintInline, RECOMMENDED_OLLAMA_PULL, resolveMaxTokens, } from '../local-llm.js';
3
+ const SYSTEM_PROMPT = 'You review one turn from a coding-agent session transcript to decide whether the HUMAN ' +
4
+ 'stated a decision or redirect mid-session (e.g. "let\'s use X instead", "go with Y", ' +
5
+ '"decided to switch to Z"). CONTEXT is the agent\'s reply and is background only - it tells ' +
6
+ 'you what was actually resolved, but the thing being reviewed is the human\'s turn, not the ' +
7
+ 'agent\'s. Respond ONLY with compact JSON: ' +
8
+ '{"isDecision": boolean, "title": short string naming what was decided, "confidence": number 0-1}. ' +
9
+ 'Set isDecision to false when HUMAN_TEXT is a question, a bug report, a command, or anything ' +
10
+ 'else that is not actually a decision or redirect.';
11
+ function buildUserPrompt(candidate) {
12
+ return `HUMAN_TEXT: ${candidate.humanText}\n\nCONTEXT (the agent's reply, background only): ${candidate.contextText ?? '(none)'}`;
13
+ }
14
+ function parseConfirmation(text) {
15
+ const match = text.match(/\{[\s\S]*\}/);
16
+ if (!match)
17
+ return null;
18
+ try {
19
+ const obj = JSON.parse(match[0]);
20
+ if (typeof obj.isDecision !== 'boolean')
21
+ return null;
22
+ const confidence = typeof obj.confidence === 'number' ? Math.max(0, Math.min(1, obj.confidence)) : 0.5;
23
+ const title = typeof obj.title === 'string' ? obj.title.trim() : '';
24
+ return { isDecision: obj.isDecision, title, confidence };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * A per-reason hint, not one collapsed "no LLM configured" line - align-stack's own
32
+ * local-gateway-client.ts already documents why collapsing this union is wrong (ALI-420: "an
33
+ * unvetted local model gets its own remedy... nonsense to someone already running one") and
34
+ * ALI-692 for provider_stopped naming the model that actually failed. Only `no_llm_key` means
35
+ * nothing is configured; the other three all require a provider that DID answer or a model
36
+ * that IS installed, so telling that caller to "set a key or run Ollama" is actively wrong.
37
+ */
38
+ export function describeConfirmFailure(reason, failure) {
39
+ switch (reason) {
40
+ case 'no_llm_key':
41
+ return noProviderHintInline('these can be confirmed');
42
+ case 'unvetted_local_model':
43
+ return ` Ollama is running, but no recognised model is installed: \`ollama pull ${RECOMMENDED_OLLAMA_PULL}\`, or set ALIGN_OLLAMA_MODEL to name your own.`;
44
+ case 'confirm_error':
45
+ return failure?.kind === 'provider_stopped'
46
+ ? ` ${failure.model} (${failure.provider}) returned an unusable response (${failure.detail}) - try again.`
47
+ : ' The configured provider did not answer usably - try again.';
48
+ case 'confirm_unparseable':
49
+ return ' The model replied without usable JSON - this is usually transient; try again.';
50
+ }
51
+ }
52
+ export async function confirmFreeTextCandidate(candidate) {
53
+ const maxTokens = resolveMaxTokens('ALIGN_CLASSIFIER_MAX_TOKENS', CLASSIFIER_MAX_TOKENS);
54
+ const result = await callChatDetailed(SYSTEM_PROMPT, buildUserPrompt(candidate), {
55
+ temperature: DETERMINISTIC_TEMPERATURE,
56
+ maxTokens,
57
+ });
58
+ if (!result.ok) {
59
+ const reason = result.failure.kind === 'unrecognised_local_models' ? 'unvetted_local_model'
60
+ : result.failure.kind === 'provider_stopped' ? 'confirm_error'
61
+ : hasConfiguredProvider() ? 'confirm_error' : 'no_llm_key';
62
+ return { ok: false, reason, failure: result.failure };
63
+ }
64
+ const parsed = parseConfirmation(result.text);
65
+ if (!parsed)
66
+ return { ok: false, reason: 'confirm_unparseable' };
67
+ if (!parsed.isDecision)
68
+ return { ok: true, decision: null };
69
+ return {
70
+ ok: true,
71
+ decision: {
72
+ agent: candidate.agent,
73
+ sessionId: candidate.sessionId,
74
+ messageId: candidate.messageId,
75
+ humanText: candidate.humanText,
76
+ title: parsed.title || candidate.humanText,
77
+ confidence: parsed.confidence,
78
+ timestamp: candidate.timestamp,
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=confirm-freetext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirm-freetext.js","sourceRoot":"","sources":["../../../src/lib/sessions/confirm-freetext.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { AgentName, CanonicalSession } from './types.js';
2
+ /**
3
+ * ALI-809 Pass B, heuristic half: a HUMAN turn whose own words redirect or settle on an
4
+ * approach mid-session. Everything downstream (confirm-freetext.ts) treats this as a proposal
5
+ * only - "heuristic proposes, LLM adjudicates, only adjudicated results assert" is the same
6
+ * shape as decision_links' writer split (align decision ALI-409), applied here to candidates
7
+ * instead of edges.
8
+ */
9
+ export interface RawFreeTextCandidate {
10
+ agent: AgentName;
11
+ sessionId: string;
12
+ /** `turn-<index>` into the parsed session's turns array - stable for one session file, and
13
+ * used only as an object-reference-free identity in the source_url; nothing here needs it
14
+ * to survive a re-parse of a DIFFERENT file. */
15
+ messageId: string;
16
+ /** The human's own words, verbatim - the only decider-authored content in this candidate. */
17
+ humanText: string;
18
+ /** The assistant turn immediately after, when there is one - CONTEXT for the LLM confirmation
19
+ * pass to read, never itself scanned for the trigger phrase and never the source of the
20
+ * claim (the same "don't let bot content pass as the human's own statement" principle as
21
+ * align decision ALI-538, applied to a different pipeline). */
22
+ contextText: string | null;
23
+ timestamp: string | null;
24
+ }
25
+ export declare function findFreeTextCandidates(session: CanonicalSession): RawFreeTextCandidate[];
26
+ //# sourceMappingURL=extract-freetext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-freetext.d.ts","sourceRoot":"","sources":["../../../src/lib/sessions/extract-freetext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB;;qDAEiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,SAAS,EAAE,MAAM,CAAC;IAClB;;;oEAGgE;IAChE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAsCD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,oBAAoB,EAAE,CAgBxF"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Phrases a human types when redirecting or settling on an approach mid-session. Calibrated
3
+ * against four REAL captured sessions (fixtures/sessions/), not invented - see
4
+ * extract-freetext.test.ts's end-to-end block for which fixture drove which pattern. A miss
5
+ * here is a missed candidate, not a wrong one (the LLM pass only ever narrows further), so
6
+ * staying narrow and growing this as more real sessions are captured beats guessing at
7
+ * phrasings nobody has actually said.
8
+ */
9
+ const DECISION_PHRASES = [
10
+ /\binstead of\b/i,
11
+ /\blet'?s (go with|use|do|switch to|just use|just go with)\b/i,
12
+ /\bdecid(e|ed|ing|ision)\b/i,
13
+ /\bgo with\b/i,
14
+ /\bswitch(ed|ing)? to\b/i,
15
+ ];
16
+ function isDecisionShaped(text) {
17
+ return DECISION_PHRASES.some(p => p.test(text));
18
+ }
19
+ /**
20
+ * The assistant's reply to a human's redirect is rarely one turn: a commentary aside, a tool
21
+ * call, and the actual resolution routinely land as separate turns in the canonical shape
22
+ * (confirmed against the real codex and opencode fixtures - a tool-call turn carries text: ''
23
+ * and sits between the commentary and the resolution in both). Walks every CONSECUTIVE
24
+ * assistant turn starting at `from`, joining the non-empty ones, and stops at the next human
25
+ * turn or the end of the session - never crossing into a later human turn's own reply.
26
+ */
27
+ function collectAssistantContext(turns, from) {
28
+ const parts = [];
29
+ for (let i = from; i < turns.length && turns[i].role === 'assistant'; i++) {
30
+ if (turns[i].text)
31
+ parts.push(turns[i].text);
32
+ }
33
+ return parts.length ? parts.join('\n') : null;
34
+ }
35
+ export function findFreeTextCandidates(session) {
36
+ const candidates = [];
37
+ session.turns.forEach((turn, i) => {
38
+ // toolCalls-only turns carry text: '' (types.ts), which no phrase can match - no special
39
+ // case needed beyond the phrase test itself.
40
+ if (turn.role !== 'user' || !isDecisionShaped(turn.text))
41
+ return;
42
+ candidates.push({
43
+ agent: session.agent,
44
+ sessionId: session.sessionId,
45
+ messageId: `turn-${i}`,
46
+ humanText: turn.text,
47
+ contextText: collectAssistantContext(session.turns, i + 1),
48
+ timestamp: turn.timestamp,
49
+ });
50
+ });
51
+ return candidates;
52
+ }
53
+ //# sourceMappingURL=extract-freetext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-freetext.js","sourceRoot":"","sources":["../../../src/lib/sessions/extract-freetext.ts"],"names":[],"mappings":"AA0BA;;;;;;;GAOG;AACH,MAAM,gBAAgB,GAAsB;IAC1C,iBAAiB;IACjB,8DAA8D;IAC9D,4BAA4B;IAC5B,cAAc;IACd,yBAAyB;CAC1B,CAAC;AAEF,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,uBAAuB,CAAC,KAAgC,EAAE,IAAY;IAC7E,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1E,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAyB;IAC9D,MAAM,UAAU,GAA2B,EAAE,CAAC;IAC9C,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAChC,yFAAyF;QACzF,6CAA6C;QAC7C,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QACjE,UAAU,CAAC,IAAI,CAAC;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,SAAS,EAAE,QAAQ,CAAC,EAAE;YACtB,SAAS,EAAE,IAAI,CAAC,IAAI;YACpB,WAAW,EAAE,uBAAuB,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;YAC1D,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Chars per token, measured (2026-09-03 research doc) with Anthropic's count_tokens against a
3
+ * rendered decision corpus: 2.56 on 4.7+ models, 3.43 on older ones. The SMALLER ratio is the
4
+ * conservative one - it estimates MORE tokens per char - so Phase 0 uses it for every family.
5
+ * Never chars/4: that under-counts our corpus by ~36% and the under-count is silent.
6
+ * The per-family table is ALI-852.
7
+ */
8
+ export declare const CHARS_PER_TOKEN = 2.56;
9
+ export declare const ESTIMATE_MARGIN = 1.1;
10
+ /** Conservative token estimate for a string: rounds UP, and adds a 10% margin on top. */
11
+ export declare function estimateTokens(text: string): number;
12
+ /**
13
+ * The char budget that fits `tokens`, inverse of estimateTokens. Rounds DOWN, so a caller
14
+ * that slices text to `charsForTokens(n)` characters never re-estimates above `n`.
15
+ */
16
+ export declare function charsForTokens(tokens: number): number;
17
+ //# sourceMappingURL=token-estimate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-estimate.d.ts","sourceRoot":"","sources":["../../src/lib/token-estimate.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,24 @@
1
+ // ALI-845 Phase 0: no token-estimation helper existed anywhere in the CLI, so every
2
+ // prompt was assembled by concatenating decision text with no cap. This is the
3
+ // conservative estimator for that phase - a real per-family table is ALI-852's job.
4
+ /**
5
+ * Chars per token, measured (2026-09-03 research doc) with Anthropic's count_tokens against a
6
+ * rendered decision corpus: 2.56 on 4.7+ models, 3.43 on older ones. The SMALLER ratio is the
7
+ * conservative one - it estimates MORE tokens per char - so Phase 0 uses it for every family.
8
+ * Never chars/4: that under-counts our corpus by ~36% and the under-count is silent.
9
+ * The per-family table is ALI-852.
10
+ */
11
+ export const CHARS_PER_TOKEN = 2.56;
12
+ export const ESTIMATE_MARGIN = 1.1;
13
+ /** Conservative token estimate for a string: rounds UP, and adds a 10% margin on top. */
14
+ export function estimateTokens(text) {
15
+ return Math.ceil((text.length / CHARS_PER_TOKEN) * ESTIMATE_MARGIN);
16
+ }
17
+ /**
18
+ * The char budget that fits `tokens`, inverse of estimateTokens. Rounds DOWN, so a caller
19
+ * that slices text to `charsForTokens(n)` characters never re-estimates above `n`.
20
+ */
21
+ export function charsForTokens(tokens) {
22
+ return Math.floor((tokens * CHARS_PER_TOKEN) / ESTIMATE_MARGIN);
23
+ }
24
+ //# sourceMappingURL=token-estimate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-estimate.js","sourceRoot":"","sources":["../../src/lib/token-estimate.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,+EAA+E;AAC/E,oFAAoF;AAEpF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAC;AACpC,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,yFAAyF;AACzF,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC;AAClE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aligndottech/cli",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "mcpName": "io.github.aligndottech/cli",
5
5
  "type": "module",
6
6
  "description": "Align CLI - capture decisions, check alignment, and manage connectors",
@@ -65,6 +65,15 @@
65
65
  "optionalDependencies": {
66
66
  "@huggingface/transformers": "^4.2.0"
67
67
  },
68
+ "overrides": {
69
+ "hono": "4.13.7",
70
+ "@hono/node-server": "1.19.17",
71
+ "undici": "6.28.1",
72
+ "fast-uri": "3.1.7",
73
+ "ip-address": "10.7.0",
74
+ "qs": "6.16.0",
75
+ "body-parser": "2.3.0"
76
+ },
68
77
  "engines": {
69
78
  "node": ">=22.16"
70
79
  },