friction-log 0.0.0-stage → 0.6.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 (107) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +98 -2
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +504 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/bilanz.d.ts +23 -0
  8. package/dist/commands/bilanz.js +106 -0
  9. package/dist/commands/bilanz.js.map +1 -0
  10. package/dist/commands/digest.d.ts +36 -0
  11. package/dist/commands/digest.js +288 -0
  12. package/dist/commands/digest.js.map +1 -0
  13. package/dist/commands/export.d.ts +36 -0
  14. package/dist/commands/export.js +118 -0
  15. package/dist/commands/export.js.map +1 -0
  16. package/dist/commands/file.d.ts +18 -0
  17. package/dist/commands/file.js +51 -0
  18. package/dist/commands/file.js.map +1 -0
  19. package/dist/commands/import.d.ts +31 -0
  20. package/dist/commands/import.js +283 -0
  21. package/dist/commands/import.js.map +1 -0
  22. package/dist/commands/init.d.ts +44 -0
  23. package/dist/commands/init.js +256 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/list.d.ts +16 -0
  26. package/dist/commands/list.js +57 -0
  27. package/dist/commands/list.js.map +1 -0
  28. package/dist/commands/log.d.ts +18 -0
  29. package/dist/commands/log.js +52 -0
  30. package/dist/commands/log.js.map +1 -0
  31. package/dist/commands/rm.d.ts +9 -0
  32. package/dist/commands/rm.js +22 -0
  33. package/dist/commands/rm.js.map +1 -0
  34. package/dist/commands/scan.d.ts +25 -0
  35. package/dist/commands/scan.js +73 -0
  36. package/dist/commands/scan.js.map +1 -0
  37. package/dist/commands/search.d.ts +15 -0
  38. package/dist/commands/search.js +37 -0
  39. package/dist/commands/search.js.map +1 -0
  40. package/dist/commands/sync-export.d.ts +46 -0
  41. package/dist/commands/sync-export.js +80 -0
  42. package/dist/commands/sync-export.js.map +1 -0
  43. package/dist/commands/update.d.ts +12 -0
  44. package/dist/commands/update.js +24 -0
  45. package/dist/commands/update.js.map +1 -0
  46. package/dist/config.d.ts +31 -0
  47. package/dist/config.js +145 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/db.d.ts +81 -0
  50. package/dist/db.js +548 -0
  51. package/dist/db.js.map +1 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.js +16 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/paths.d.ts +12 -0
  56. package/dist/paths.js +34 -0
  57. package/dist/paths.js.map +1 -0
  58. package/dist/scanners/claude-code.d.ts +6 -0
  59. package/dist/scanners/claude-code.js +202 -0
  60. package/dist/scanners/claude-code.js.map +1 -0
  61. package/dist/scanners/index.d.ts +4 -0
  62. package/dist/scanners/index.js +12 -0
  63. package/dist/scanners/index.js.map +1 -0
  64. package/dist/sinks/agent-tasks.d.ts +36 -0
  65. package/dist/sinks/agent-tasks.js +153 -0
  66. package/dist/sinks/agent-tasks.js.map +1 -0
  67. package/dist/sinks/github-issues.d.ts +20 -0
  68. package/dist/sinks/github-issues.js +98 -0
  69. package/dist/sinks/github-issues.js.map +1 -0
  70. package/dist/sinks/index.d.ts +11 -0
  71. package/dist/sinks/index.js +48 -0
  72. package/dist/sinks/index.js.map +1 -0
  73. package/dist/sinks/linear.d.ts +25 -0
  74. package/dist/sinks/linear.js +157 -0
  75. package/dist/sinks/linear.js.map +1 -0
  76. package/dist/sinks/markdown-file.d.ts +7 -0
  77. package/dist/sinks/markdown-file.js +54 -0
  78. package/dist/sinks/markdown-file.js.map +1 -0
  79. package/dist/sinks/stdout-json.d.ts +15 -0
  80. package/dist/sinks/stdout-json.js +49 -0
  81. package/dist/sinks/stdout-json.js.map +1 -0
  82. package/dist/templates/auth-expiry.yml +30 -0
  83. package/dist/templates/doc-gap.yml +30 -0
  84. package/dist/templates/output-overflow.yml +25 -0
  85. package/dist/templates/schema-drift.yml +26 -0
  86. package/dist/templates/tool-error.yml +34 -0
  87. package/dist/templates/tool-missing-capability.yml +26 -0
  88. package/dist/templates/workflow-friction.yml +26 -0
  89. package/dist/templates.d.ts +5 -0
  90. package/dist/templates.js +88 -0
  91. package/dist/templates.js.map +1 -0
  92. package/dist/types.d.ts +79 -0
  93. package/dist/types.js +2 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/commands.md +74 -0
  96. package/docs/design.md +25 -0
  97. package/docs/sinks.md +79 -0
  98. package/docs/storage.md +20 -0
  99. package/docs/sync-export.md +23 -0
  100. package/package.json +64 -4
  101. package/templates/auth-expiry.yml +30 -0
  102. package/templates/doc-gap.yml +30 -0
  103. package/templates/output-overflow.yml +25 -0
  104. package/templates/schema-drift.yml +26 -0
  105. package/templates/tool-error.yml +34 -0
  106. package/templates/tool-missing-capability.yml +26 -0
  107. package/templates/workflow-friction.yml +26 -0
@@ -0,0 +1,202 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ const FRICTION_PHRASE_PATTERNS = [
3
+ /\bdidn['']?t work\b/i,
4
+ /\bdoesn['']?t work\b/i,
5
+ /\bnot working\b/i,
6
+ /\bunexpected(ly)?\b/i,
7
+ /\bblocked by\b/i,
8
+ /\bfunktioniert nicht\b/i,
9
+ /\bklappt nicht\b/i,
10
+ /\bunerwartet\b/i,
11
+ /\bschl[äa]gt fehl\b/i,
12
+ ];
13
+ const MAX_PHRASE_LINE_CHARS = 280;
14
+ export class ClaudeCodeAdapter {
15
+ name = "claude-code";
16
+ async scan(input) {
17
+ const transcriptPath = input.transcriptPath;
18
+ if (!transcriptPath) {
19
+ throw new Error("friction-log: claude-code adapter requires --transcript <path>");
20
+ }
21
+ if (!existsSync(transcriptPath)) {
22
+ throw new Error(`friction-log: transcript not found at ${transcriptPath}`);
23
+ }
24
+ const raw = readFileSync(transcriptPath, "utf8");
25
+ const lines = raw.split("\n");
26
+ const entries = [];
27
+ for (const line of lines) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed)
30
+ continue;
31
+ try {
32
+ entries.push(JSON.parse(trimmed));
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ }
38
+ const sessionId = input.sessionId ?? deriveSessionIdFromPath(transcriptPath);
39
+ const { startedAt, endedAt, cwds } = sessionBounds(entries);
40
+ const candidates = extractCandidates(entries);
41
+ return {
42
+ session: {
43
+ id: sessionId,
44
+ startedAt,
45
+ endedAt,
46
+ projectPaths: cwds.length > 0 ? cwds : null,
47
+ transcriptPath,
48
+ },
49
+ frictionCandidates: candidates,
50
+ };
51
+ }
52
+ }
53
+ function deriveSessionIdFromPath(transcriptPath) {
54
+ const base = transcriptPath.replace(/\\/g, "/").split("/").pop() ?? transcriptPath;
55
+ return base.replace(/\.jsonl$/i, "");
56
+ }
57
+ function sessionBounds(entries) {
58
+ let startedAt = null;
59
+ let endedAt = null;
60
+ const cwds = new Set();
61
+ for (const e of entries) {
62
+ const ts = isMessageEntry(e) ? e.timestamp : undefined;
63
+ if (ts) {
64
+ if (!startedAt || ts < startedAt)
65
+ startedAt = ts;
66
+ if (!endedAt || ts > endedAt)
67
+ endedAt = ts;
68
+ }
69
+ if (isMessageEntry(e) && typeof e.cwd === "string" && e.cwd.length > 0) {
70
+ cwds.add(e.cwd);
71
+ }
72
+ }
73
+ return {
74
+ startedAt: startedAt ?? fileMtimeIso(),
75
+ endedAt,
76
+ cwds: [...cwds],
77
+ };
78
+ }
79
+ function fileMtimeIso() {
80
+ return new Date().toISOString();
81
+ }
82
+ function isMessageEntry(e) {
83
+ return e.type === "user" || e.type === "assistant";
84
+ }
85
+ function extractCandidates(entries) {
86
+ const toolUseById = new Map();
87
+ const out = [];
88
+ const seenTitles = new Set();
89
+ for (const entry of entries) {
90
+ if (!isMessageEntry(entry))
91
+ continue;
92
+ const blocks = normalizeContent(entry.message?.content);
93
+ for (const block of blocks) {
94
+ if (block.type === "tool_use") {
95
+ const tu = block;
96
+ toolUseById.set(tu.id, tu.name);
97
+ }
98
+ else if (block.type === "tool_result") {
99
+ const tr = block;
100
+ if (!tr.is_error)
101
+ continue;
102
+ const toolName = toolUseById.get(tr.tool_use_id) ?? "unknown-tool";
103
+ const errText = stringifyContent(tr.content);
104
+ const candidate = toolErrorCandidate(toolName, errText);
105
+ addUnique(out, seenTitles, candidate);
106
+ }
107
+ else if (block.type === "text" && entry.type === "assistant") {
108
+ const text = block.text ?? "";
109
+ for (const c of phraseCandidates(text)) {
110
+ addUnique(out, seenTitles, c);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ function addUnique(out, seen, candidate) {
118
+ const key = `${candidate.toolSurface ?? ""}|${candidate.title}`;
119
+ if (seen.has(key))
120
+ return;
121
+ seen.add(key);
122
+ out.push(candidate);
123
+ }
124
+ function normalizeContent(content) {
125
+ if (!content)
126
+ return [];
127
+ if (typeof content === "string")
128
+ return [{ type: "text", text: content }];
129
+ return content;
130
+ }
131
+ function stringifyContent(content) {
132
+ if (typeof content === "string")
133
+ return content;
134
+ if (Array.isArray(content)) {
135
+ return content
136
+ .map((c) => {
137
+ if (typeof c === "string")
138
+ return c;
139
+ if (c && typeof c === "object" && "text" in c)
140
+ return String(c.text);
141
+ return JSON.stringify(c);
142
+ })
143
+ .join("\n");
144
+ }
145
+ if (content == null)
146
+ return "";
147
+ return JSON.stringify(content);
148
+ }
149
+ function toolErrorCandidate(toolName, errText) {
150
+ const firstLine = errText.split("\n")[0]?.trim() ?? "";
151
+ const exitMatch = /^Exit code (\d+)/i.exec(firstLine);
152
+ if (toolName === "Bash" && exitMatch) {
153
+ const code = exitMatch[1];
154
+ return {
155
+ toolSurface: "bash",
156
+ title: `Bash exited with code ${code}`,
157
+ description: truncate(errText, 2000),
158
+ severity: "medium",
159
+ category: "tool-error",
160
+ };
161
+ }
162
+ const summary = truncate(firstLine || errText, 120);
163
+ return {
164
+ toolSurface: toolName,
165
+ title: `${toolName} returned an error: ${summary}`,
166
+ description: truncate(errText, 2000),
167
+ severity: "medium",
168
+ category: "tool-error",
169
+ };
170
+ }
171
+ function phraseCandidates(text) {
172
+ const candidates = [];
173
+ const paragraphs = text.split(/\n{2,}/);
174
+ for (const para of paragraphs) {
175
+ const trimmed = para.trim();
176
+ if (trimmed.length === 0 || trimmed.length > MAX_PHRASE_LINE_CHARS)
177
+ continue;
178
+ if (trimmed.endsWith("?"))
179
+ continue;
180
+ for (const pattern of FRICTION_PHRASE_PATTERNS) {
181
+ const m = pattern.exec(trimmed);
182
+ if (m) {
183
+ candidates.push({
184
+ toolSurface: null,
185
+ title: `friction phrase: ${truncate(trimmed, 100)}`,
186
+ description: trimmed,
187
+ severity: "low",
188
+ category: "workflow-friction",
189
+ });
190
+ break;
191
+ }
192
+ }
193
+ }
194
+ return candidates;
195
+ }
196
+ function truncate(s, max) {
197
+ return s.length <= max ? s : s.slice(0, max - 1) + "…";
198
+ }
199
+ export function fileExistsCheck(p) {
200
+ return existsSync(p) && statSync(p).isFile();
201
+ }
202
+ //# sourceMappingURL=claude-code.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/scanners/claude-code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAkD7D,MAAM,wBAAwB,GAAa;IACzC,sBAAsB;IACtB,uBAAuB;IACvB,kBAAkB;IAClB,sBAAsB;IACtB,iBAAiB;IACjB,yBAAyB;IACzB,mBAAmB;IACnB,iBAAiB;IACjB,sBAAsB;CACvB,CAAC;AAEF,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAUlC,MAAM,OAAO,iBAAiB;IACnB,IAAI,GAAG,aAAa,CAAC;IAE9B,KAAK,CAAC,IAAI,CAAC,KAAmB;QAC5B,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC;QAC5C,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CACb,yCAAyC,cAAc,EAAE,CAC1D,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAsB,EAAE,CAAC;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO;gBAAE,SAAS;YACvB,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,CAAC,CAAC;YACvD,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GACb,KAAK,CAAC,SAAS,IAAI,uBAAuB,CAAC,cAAc,CAAC,CAAC;QAC7D,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QAC5D,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAE9C,OAAO;YACL,OAAO,EAAE;gBACP,EAAE,EAAE,SAAS;gBACb,SAAS;gBACT,OAAO;gBACP,YAAY,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;gBAC3C,cAAc;aACf;YACD,kBAAkB,EAAE,UAAU;SAC/B,CAAC;IACJ,CAAC;CACF;AAED,SAAS,uBAAuB,CAAC,cAAsB;IACrD,MAAM,IAAI,GACR,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,cAAc,CAAC;IACxE,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,aAAa,CAAC,OAA0B;IAK/C,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;QACvD,IAAI,EAAE,EAAE,CAAC;YACP,IAAI,CAAC,SAAS,IAAI,EAAE,GAAG,SAAS;gBAAE,SAAS,GAAG,EAAE,CAAC;YACjD,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,OAAO;gBAAE,OAAO,GAAG,EAAE,CAAC;QAC7C,CAAC;QACD,IAAI,cAAc,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO;QACL,SAAS,EAAE,SAAS,IAAI,YAAY,EAAE;QACtC,OAAO;QACP,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;KAChB,CAAC;AACJ,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,cAAc,CAAC,CAAkB;IACxC,OAAO,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC;AACrD,CAAC;AAED,SAAS,iBAAiB,CAAC,OAA0B;IACnD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IAErC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;YAAE,SAAS;QACrC,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAExD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAG,KAAqB,CAAC;gBACjC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;YAClC,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBACxC,MAAM,EAAE,GAAG,KAAwB,CAAC;gBACpC,IAAI,CAAC,EAAE,CAAC,QAAQ;oBAAE,SAAS;gBAC3B,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,cAAc,CAAC;gBACnE,MAAM,OAAO,GAAG,gBAAgB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;gBAC7C,MAAM,SAAS,GAAG,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBACxD,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;YACxC,CAAC;iBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBAC/D,MAAM,IAAI,GAAI,KAAmB,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC7C,KAAK,MAAM,CAAC,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;oBACvC,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAChB,GAA2B,EAC3B,IAAiB,EACjB,SAA+B;IAE/B,MAAM,GAAG,GAAG,GAAG,SAAS,CAAC,WAAW,IAAI,EAAE,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;IAChE,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IAC1B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,gBAAgB,CACvB,OAA4C;IAE5C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,gBAAgB,CAAC,OAAgB;IACxC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,OAAO;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACT,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,OAAO,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,IAAI,CAAC;gBAC3C,OAAO,MAAM,CAAE,CAAuB,CAAC,IAAI,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3B,CAAC,CAAC;aACD,IAAI,CAAC,IAAI,CAAC,CAAC;IAChB,CAAC;IACD,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC/B,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,kBAAkB,CACzB,QAAgB,EAChB,OAAe;IAEf,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACvD,MAAM,SAAS,GAAG,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtD,IAAI,QAAQ,KAAK,MAAM,IAAI,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC1B,OAAO;YACL,WAAW,EAAE,MAAM;YACnB,KAAK,EAAE,yBAAyB,IAAI,EAAE;YACtC,WAAW,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;YACpC,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,YAAY;SACvB,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,IAAI,OAAO,EAAE,GAAG,CAAC,CAAC;IACpD,OAAO;QACL,WAAW,EAAE,QAAQ;QACrB,KAAK,EAAE,GAAG,QAAQ,uBAAuB,OAAO,EAAE;QAClD,WAAW,EAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;QACpC,QAAQ,EAAE,QAAQ;QAClB,QAAQ,EAAE,YAAY;KACvB,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY;IACpC,MAAM,UAAU,GAA2B,EAAE,CAAC;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,qBAAqB;YAChE,SAAS;QACX,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QACpC,KAAK,MAAM,OAAO,IAAI,wBAAwB,EAAE,CAAC;YAC/C,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAChC,IAAI,CAAC,EAAE,CAAC;gBACN,UAAU,CAAC,IAAI,CAAC;oBACd,WAAW,EAAE,IAAI;oBACjB,KAAK,EAAE,oBAAoB,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE;oBACnD,WAAW,EAAE,OAAO;oBACpB,QAAQ,EAAE,KAAK;oBACf,QAAQ,EAAE,mBAAmB;iBAC9B,CAAC,CAAC;gBACH,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Scanner } from "../types.js";
2
+ export type ScannerName = "claude-code";
3
+ export declare function loadScanner(name: string): Scanner;
4
+ export declare const availableScanners: readonly string[];
@@ -0,0 +1,12 @@
1
+ import { ClaudeCodeAdapter } from "./claude-code.js";
2
+ export function loadScanner(name) {
3
+ switch (name) {
4
+ case "claude-code":
5
+ return new ClaudeCodeAdapter();
6
+ default:
7
+ throw new Error(`friction-log: unknown scanner "${name}". Available in M2: claude-code. ` +
8
+ `Other adapters (cursor, aider, opencode) are planned for later milestones.`);
9
+ }
10
+ }
11
+ export const availableScanners = ["claude-code"];
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scanners/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,aAAa;YAChB,OAAO,IAAI,iBAAiB,EAAE,CAAC;QACjC;YACE,MAAM,IAAI,KAAK,CACb,kCAAkC,IAAI,mCAAmC;gBACvE,4EAA4E,CAC/E,CAAC;IACN,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAsB,CAAC,aAAa,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { FileOptions, FileResult, Friction, Priority, RenderedTemplate, Sink } from "../types.js";
2
+ export interface AgentTasksOpts {
3
+ /** Defaults to "rest". "mcp-emit" prints the equivalent
4
+ * mcp__agent-tasks__task_create JSON to stdout instead of POSTing.
5
+ */
6
+ mode: "rest" | "mcp-emit";
7
+ projectId?: string;
8
+ /** REST base URL, e.g. https://agent-tasks.opentriologue.ai */
9
+ apiBase?: string;
10
+ /** Bearer token. Defaults to env var AGENT_TASKS_TOKEN. */
11
+ token?: string;
12
+ priority?: Priority;
13
+ labels?: string[];
14
+ }
15
+ export type Fetcher = (url: string, init: RequestInit) => Promise<Response>;
16
+ export type StdoutWriter = (line: string) => void;
17
+ /**
18
+ * agent-tasks sink. Two paths, both supported per spec:
19
+ *
20
+ * - mode=rest (default): POST /api/projects/<id>/tasks with the friction's
21
+ * rendered title/body, returns the new task UUID as external_ref.
22
+ * - mode=mcp-emit: print the equivalent mcp__agent-tasks__task_create JSON
23
+ * to stdout for an agent harness to pick up and execute. No network call
24
+ * is made. Useful when friction-log is run inside a Claude Code session
25
+ * where the agent already holds the MCP scope.
26
+ *
27
+ * mcp-emit is the honest version of "the MCP path" for a standalone Node CLI:
28
+ * it doesn't pretend to speak MCP, it just emits the structured intent.
29
+ */
30
+ export declare class AgentTasksSink implements Sink {
31
+ private readonly fetchImpl;
32
+ private readonly write;
33
+ readonly name = "agent-tasks";
34
+ constructor(fetchImpl?: Fetcher, write?: StdoutWriter);
35
+ file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
36
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * agent-tasks sink. Two paths, both supported per spec:
3
+ *
4
+ * - mode=rest (default): POST /api/projects/<id>/tasks with the friction's
5
+ * rendered title/body, returns the new task UUID as external_ref.
6
+ * - mode=mcp-emit: print the equivalent mcp__agent-tasks__task_create JSON
7
+ * to stdout for an agent harness to pick up and execute. No network call
8
+ * is made. Useful when friction-log is run inside a Claude Code session
9
+ * where the agent already holds the MCP scope.
10
+ *
11
+ * mcp-emit is the honest version of "the MCP path" for a standalone Node CLI:
12
+ * it doesn't pretend to speak MCP, it just emits the structured intent.
13
+ */
14
+ export class AgentTasksSink {
15
+ fetchImpl;
16
+ write;
17
+ name = "agent-tasks";
18
+ constructor(fetchImpl = (url, init) => fetch(url, init), write = (line) => process.stdout.write(line)) {
19
+ this.fetchImpl = fetchImpl;
20
+ this.write = write;
21
+ }
22
+ async file(friction, rendered, opts) {
23
+ const parsed = parseOpts(opts);
24
+ const payload = buildPayload(friction, rendered, parsed);
25
+ if (parsed.mode === "mcp-emit") {
26
+ const intent = {
27
+ tool: "mcp__agent-tasks__task_create",
28
+ params: payload,
29
+ };
30
+ this.write(JSON.stringify(intent) + "\n");
31
+ return {
32
+ ok: true,
33
+ sinkTarget: `agent-tasks:mcp-emit:${payload.projectId}`,
34
+ message: "emitted MCP task_create intent to stdout (no network call)",
35
+ };
36
+ }
37
+ const apiBase = parsed.apiBase;
38
+ if (!apiBase) {
39
+ throw new Error(`friction-log: agent-tasks sink in REST mode requires "apiBase". Set sinks.agent-tasks.apiBase in config.yml or pass --sink-opt apiBase=https://...`);
40
+ }
41
+ const token = parsed.token ?? process.env.AGENT_TASKS_TOKEN;
42
+ if (!token) {
43
+ throw new Error(`friction-log: agent-tasks REST mode requires a token. Set AGENT_TASKS_TOKEN env var or sinks.agent-tasks.token.`);
44
+ }
45
+ const url = `${apiBase.replace(/\/$/, "")}/api/projects/${encodeURIComponent(payload.projectId)}/tasks`;
46
+ const response = await this.fetchImpl(url, {
47
+ method: "POST",
48
+ headers: {
49
+ "content-type": "application/json",
50
+ accept: "application/json",
51
+ authorization: `Bearer ${token}`,
52
+ },
53
+ body: JSON.stringify({
54
+ title: payload.title,
55
+ description: payload.description,
56
+ priority: payload.priority,
57
+ labels: payload.labels,
58
+ }),
59
+ });
60
+ if (!response.ok) {
61
+ const body = await safeText(response);
62
+ throw new Error(`friction-log: agent-tasks REST returned ${response.status}: ${body.slice(0, 400)}`);
63
+ }
64
+ const parsedBody = (await response.json());
65
+ const taskId = parsedBody.task?.id ?? parsedBody.id;
66
+ if (typeof taskId !== "string") {
67
+ throw new Error(`friction-log: agent-tasks REST response missing task id: ${JSON.stringify(parsedBody).slice(0, 400)}`);
68
+ }
69
+ return {
70
+ ok: true,
71
+ sinkTarget: `agent-tasks:${payload.projectId}`,
72
+ externalRef: taskId,
73
+ message: `created task ${taskId}`,
74
+ };
75
+ }
76
+ }
77
+ function buildPayload(friction, rendered, opts) {
78
+ if (!opts.projectId) {
79
+ throw new Error(`friction-log: agent-tasks sink requires "projectId" (uuid). Set sinks.agent-tasks.projectId in config.yml or pass --sink-opt projectId=...`);
80
+ }
81
+ const labels = Array.from(new Set([...(opts.labels ?? []), ...rendered.labels]));
82
+ const description = renderDescription(friction, rendered);
83
+ return {
84
+ projectId: opts.projectId,
85
+ title: rendered.title,
86
+ description,
87
+ priority: opts.priority ?? rendered.priority,
88
+ labels,
89
+ };
90
+ }
91
+ function parseOpts(opts) {
92
+ const raw = opts.sinkOpts ?? {};
93
+ const mode = parseMode(raw.mode);
94
+ const projectId = optionalString(raw.projectId, "projectId");
95
+ const apiBase = optionalString(raw.apiBase, "apiBase");
96
+ const token = optionalString(raw.token, "token");
97
+ const priority = parsePriority(raw.priority);
98
+ const labels = parseStringList(raw.labels, "labels");
99
+ return { mode, projectId, apiBase, token, priority, labels };
100
+ }
101
+ function parseMode(value) {
102
+ if (value == null)
103
+ return "rest";
104
+ if (value === "rest" || value === "mcp-emit")
105
+ return value;
106
+ throw new Error(`friction-log: agent-tasks "mode" must be "rest" or "mcp-emit", got ${JSON.stringify(value)}`);
107
+ }
108
+ function optionalString(value, name) {
109
+ if (value == null)
110
+ return undefined;
111
+ if (typeof value === "string")
112
+ return value;
113
+ throw new Error(`friction-log: agent-tasks "${name}" must be a string`);
114
+ }
115
+ function parsePriority(value) {
116
+ if (value == null)
117
+ return undefined;
118
+ const valid = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
119
+ if (typeof value === "string" && valid.includes(value))
120
+ return value;
121
+ throw new Error(`friction-log: agent-tasks "priority" must be one of ${valid.join("|")}`);
122
+ }
123
+ function parseStringList(value, name) {
124
+ if (value == null)
125
+ return undefined;
126
+ if (typeof value === "string")
127
+ return [value];
128
+ if (Array.isArray(value) && value.every((v) => typeof v === "string"))
129
+ return value;
130
+ throw new Error(`friction-log: agent-tasks "${name}" must be a string or array of strings`);
131
+ }
132
+ function renderDescription(friction, rendered) {
133
+ const meta = [];
134
+ meta.push(`friction-log id ${friction.id} captured ${friction.capturedAt}`);
135
+ if (friction.toolSurface)
136
+ meta.push(`tool: ${friction.toolSurface}`);
137
+ if (friction.category)
138
+ meta.push(`category: ${friction.category}`);
139
+ if (friction.severity)
140
+ meta.push(`severity: ${friction.severity}`);
141
+ if (friction.recurrenceOfId != null)
142
+ meta.push(`recurrence of friction #${friction.recurrenceOfId}`);
143
+ return `${rendered.body}\n\n---\n${meta.join("\n")}\n`;
144
+ }
145
+ async function safeText(response) {
146
+ try {
147
+ return await response.text();
148
+ }
149
+ catch {
150
+ return "(no body)";
151
+ }
152
+ }
153
+ //# sourceMappingURL=agent-tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-tasks.js","sourceRoot":"","sources":["../../src/sinks/agent-tasks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type SpawnSyncReturns } from "node:child_process";
2
+ import type { FileOptions, FileResult, Friction, RenderedTemplate, Sink } from "../types.js";
3
+ export interface GithubIssuesOpts {
4
+ repo: string;
5
+ labels?: string[];
6
+ assignee?: string;
7
+ milestone?: string;
8
+ }
9
+ export type GhRunner = (args: string[], input: string) => SpawnSyncReturns<string>;
10
+ /**
11
+ * Create a GitHub issue by spawning `gh issue create`. No HTTP client in the
12
+ * package: gh handles auth, retries, and proxy config. Required option `repo`
13
+ * is owner/name; everything else is optional.
14
+ */
15
+ export declare class GithubIssuesSink implements Sink {
16
+ private readonly runGh;
17
+ readonly name = "github-issues";
18
+ constructor(runGh?: GhRunner);
19
+ file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
20
+ }
@@ -0,0 +1,98 @@
1
+ import { spawnSync } from "node:child_process";
2
+ const defaultRunner = (args, input) => spawnSync("gh", args, {
3
+ input,
4
+ encoding: "utf8",
5
+ });
6
+ /**
7
+ * Create a GitHub issue by spawning `gh issue create`. No HTTP client in the
8
+ * package: gh handles auth, retries, and proxy config. Required option `repo`
9
+ * is owner/name; everything else is optional.
10
+ */
11
+ export class GithubIssuesSink {
12
+ runGh;
13
+ name = "github-issues";
14
+ constructor(runGh = defaultRunner) {
15
+ this.runGh = runGh;
16
+ }
17
+ async file(friction, rendered, opts) {
18
+ const parsed = parseOpts(opts);
19
+ const labels = Array.from(new Set([...(parsed.labels ?? []), ...rendered.labels]));
20
+ const args = [
21
+ "issue",
22
+ "create",
23
+ "--repo",
24
+ parsed.repo,
25
+ "--title",
26
+ rendered.title,
27
+ "--body-file",
28
+ "-",
29
+ ];
30
+ for (const label of labels) {
31
+ args.push("--label", label);
32
+ }
33
+ if (parsed.assignee)
34
+ args.push("--assignee", parsed.assignee);
35
+ if (parsed.milestone)
36
+ args.push("--milestone", parsed.milestone);
37
+ const body = renderBody(friction, rendered);
38
+ const result = this.runGh(args, body);
39
+ if (result.error) {
40
+ throw new Error(`friction-log: gh CLI not runnable: ${result.error.message}`);
41
+ }
42
+ if (result.status !== 0) {
43
+ const stderr = (result.stderr ?? "").trim();
44
+ throw new Error(`friction-log: gh issue create failed (exit ${result.status}): ${stderr || "no stderr"}`);
45
+ }
46
+ const issueUrl = (result.stdout ?? "").trim().split("\n").pop() ?? "";
47
+ if (!/^https:\/\/github\.com\//.test(issueUrl)) {
48
+ throw new Error(`friction-log: gh stdout did not contain an issue URL: ${result.stdout}`);
49
+ }
50
+ return {
51
+ ok: true,
52
+ sinkTarget: parsed.repo,
53
+ externalRef: issueUrl,
54
+ message: `created ${issueUrl}`,
55
+ };
56
+ }
57
+ }
58
+ function parseOpts(opts) {
59
+ const raw = opts.sinkOpts ?? {};
60
+ const repo = raw.repo;
61
+ if (typeof repo !== "string" || !/^[^/\s]+\/[^/\s]+$/.test(repo)) {
62
+ throw new Error(`friction-log: github-issues sink requires "repo" (owner/name). Set sinks.github-issues.repo in config.yml or pass --sink-opt repo=owner/name.`);
63
+ }
64
+ const labels = normalizeStringList(raw.labels, "labels");
65
+ const assignee = optionalString(raw.assignee, "assignee");
66
+ const milestone = optionalString(raw.milestone, "milestone");
67
+ return { repo, labels, assignee, milestone };
68
+ }
69
+ function normalizeStringList(value, name) {
70
+ if (value == null)
71
+ return undefined;
72
+ if (typeof value === "string")
73
+ return [value];
74
+ if (Array.isArray(value) && value.every((v) => typeof v === "string"))
75
+ return value;
76
+ throw new Error(`friction-log: github-issues "${name}" must be a string or array of strings`);
77
+ }
78
+ function optionalString(value, name) {
79
+ if (value == null)
80
+ return undefined;
81
+ if (typeof value === "string")
82
+ return value;
83
+ throw new Error(`friction-log: github-issues "${name}" must be a string`);
84
+ }
85
+ function renderBody(friction, rendered) {
86
+ const meta = [];
87
+ meta.push(`<sub>friction-log id ${friction.id} captured ${friction.capturedAt}</sub>`);
88
+ if (friction.toolSurface)
89
+ meta.push(`<sub>tool: \`${friction.toolSurface}\`</sub>`);
90
+ if (friction.category)
91
+ meta.push(`<sub>category: ${friction.category}</sub>`);
92
+ if (friction.severity)
93
+ meta.push(`<sub>severity: ${friction.severity}</sub>`);
94
+ if (friction.recurrenceOfId != null)
95
+ meta.push(`<sub>recurrence of friction #${friction.recurrenceOfId}</sub>`);
96
+ return `${rendered.body}\n\n---\n${meta.join(" \n")}\n`;
97
+ }
98
+ //# sourceMappingURL=github-issues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github-issues.js","sourceRoot":"","sources":["../../src/sinks/github-issues.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { Sink } from "../types.js";
2
+ export type SinkName = "markdown-file" | "stdout-json" | "github-issues" | "agent-tasks" | "linear";
3
+ export declare const availableSinks: readonly SinkName[];
4
+ type SinkFactory = () => Promise<Sink>;
5
+ export declare function loadSink(name: string): Promise<Sink>;
6
+ /**
7
+ * Replace a sink factory for the duration of a test. Returns a restore
8
+ * function. Production code never calls this.
9
+ */
10
+ export declare function registerSinkFactory(name: SinkName, factory: SinkFactory): () => void;
11
+ export {};
@@ -0,0 +1,48 @@
1
+ export const availableSinks = [
2
+ "markdown-file",
3
+ "stdout-json",
4
+ "github-issues",
5
+ "agent-tasks",
6
+ "linear",
7
+ ];
8
+ const factories = {
9
+ "markdown-file": async () => {
10
+ const m = await import("./markdown-file.js");
11
+ return new m.MarkdownFileSink();
12
+ },
13
+ "stdout-json": async () => {
14
+ const m = await import("./stdout-json.js");
15
+ return new m.StdoutJsonSink();
16
+ },
17
+ "github-issues": async () => {
18
+ const m = await import("./github-issues.js");
19
+ return new m.GithubIssuesSink();
20
+ },
21
+ "agent-tasks": async () => {
22
+ const m = await import("./agent-tasks.js");
23
+ return new m.AgentTasksSink();
24
+ },
25
+ linear: async () => {
26
+ const m = await import("./linear.js");
27
+ return new m.LinearSink();
28
+ },
29
+ };
30
+ export async function loadSink(name) {
31
+ const factory = factories[name];
32
+ if (!factory) {
33
+ throw new Error(`friction-log: unknown sink "${name}". Known sinks: ${availableSinks.join(", ")}.`);
34
+ }
35
+ return factory();
36
+ }
37
+ /**
38
+ * Replace a sink factory for the duration of a test. Returns a restore
39
+ * function. Production code never calls this.
40
+ */
41
+ export function registerSinkFactory(name, factory) {
42
+ const previous = factories[name];
43
+ factories[name] = factory;
44
+ return () => {
45
+ factories[name] = previous;
46
+ };
47
+ }
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sinks/index.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,cAAc,GAAwB;IACjD,eAAe;IACf,aAAa;IACb,eAAe;IACf,aAAa;IACb,QAAQ;CACT,CAAC;AASF,MAAM,SAAS,GAAkC;IAC/C,eAAe,EAAE,KAAK,IAAI,EAAE;QAC1B,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClC,CAAC;IACD,aAAa,EAAE,KAAK,IAAI,EAAE;QACxB,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;IAChC,CAAC;IACD,eAAe,EAAE,KAAK,IAAI,EAAE;QAC1B,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAC;QAC7C,OAAO,IAAI,CAAC,CAAC,gBAAgB,EAAE,CAAC;IAClC,CAAC;IACD,aAAa,EAAE,KAAK,IAAI,EAAE;QACxB,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;IAChC,CAAC;IACD,MAAM,EAAE,KAAK,IAAI,EAAE;QACjB,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;IAC5B,CAAC;CACF,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAgB,CAAC,CAAC;IAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,+BAA+B,IAAI,mBAAmB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACnF,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,EAAE,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAc,EACd,OAAoB;IAEpB,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACjC,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;IAC1B,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { FileOptions, FileResult, Friction, RenderedTemplate, Sink } from "../types.js";
2
+ export interface LinearOpts {
3
+ teamId: string;
4
+ state?: string;
5
+ assignee?: string;
6
+ /** LINEAR_API_KEY env var by default. */
7
+ apiKey?: string;
8
+ /** Default https://api.linear.app/graphql. */
9
+ endpoint?: string;
10
+ }
11
+ export type Fetcher = (url: string, init: RequestInit) => Promise<Response>;
12
+ /**
13
+ * Create a Linear issue via GraphQL. LINEAR_API_KEY env var supplies the
14
+ * personal access token by default; teamId is required.
15
+ *
16
+ * The `state` option is matched case-insensitively against the team's
17
+ * workflow-state names so callers can write "Backlog" instead of a UUID.
18
+ * Looking up the workflow state requires one extra query, kept minimal.
19
+ */
20
+ export declare class LinearSink implements Sink {
21
+ private readonly fetchImpl;
22
+ readonly name = "linear";
23
+ constructor(fetchImpl?: Fetcher);
24
+ file(friction: Friction, rendered: RenderedTemplate, opts: FileOptions): Promise<FileResult>;
25
+ }