@bububuger/spanory 0.1.18 → 0.1.19

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bububuger/spanory",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Spanory CLI for cross-runtime agent observability",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,16 +14,19 @@
14
14
  "registry": "https://registry.npmjs.org"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist/index.js",
18
+ "dist/index.d.ts"
18
19
  ],
19
20
  "bin": {
20
21
  "spanory": "dist/index.js"
21
22
  },
22
23
  "scripts": {
23
24
  "check": "npm run --workspace @bububuger/core build && tsc -p tsconfig.runtime.json --noEmit",
24
- "build": "npm run --workspace @bububuger/core build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.runtime.json && chmod +x dist/index.js",
25
+ "build:deps": "npm run --workspace @bububuger/core build && npm run --workspace @bububuger/otlp-core build && npm run --workspace @bububuger/backend-langfuse build",
26
+ "build": "npm run build:deps && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.runtime.json && npx esbuild dist/index.js --bundle --platform=node --format=esm --target=node18 --external:node:* --external:commander --outfile=dist/index.bundle.js && node -e \"require('node:fs').renameSync('dist/index.bundle.js','dist/index.js')\" && chmod +x dist/index.js",
25
27
  "test": "npm run build && vitest run test/unit",
26
28
  "test:bdd": "npm run build && vitest run test/bdd",
29
+ "pack:test-install": "bash test/pack-test-install.sh",
27
30
  "test:golden:update": "npm run build && node test/fixtures/golden/otlp/update-golden.mjs && node test/fixtures/golden/codex/update-golden.mjs",
28
31
  "build:bundle": "npm run --workspace @bububuger/backend-langfuse build && npm run --workspace @bububuger/otlp-core build && npm run build && mkdir -p build && esbuild dist/index.js --bundle --platform=node --format=cjs --target=node18 --define:process.env.SPANORY_VERSION=\\\"$npm_package_version\\\" --outfile=build/spanory.cjs",
29
32
  "build:bin:macos-arm64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-macos-arm64 --output ../../dist/spanory-macos-arm64",
@@ -32,7 +35,6 @@
32
35
  "build:bin:win-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-win-x64 --output ../../dist/spanory-win-x64.exe"
33
36
  },
34
37
  "dependencies": {
35
- "@bububuger/core": "^0.1.1",
36
38
  "commander": "^14.0.1"
37
39
  },
38
40
  "devDependencies": {
@@ -1,3 +0,0 @@
1
- export declare function loadAlertRules(path: any): Promise<any>;
2
- export declare function evaluateRules(rules: any, sessions: any): any[];
3
- export declare function sendAlertWebhook(url: any, payload: any, headers?: {}): Promise<void>;
@@ -1,348 +0,0 @@
1
- // @ts-nocheck
2
- import { readFile } from 'node:fs/promises';
3
- import { summarizeAgents, summarizeCache, summarizeCommands, summarizeMcp, summarizeSessions, summarizeTurnDiff, } from '../report/aggregate.js';
4
- const DEFAULT_CONTEXT_WINDOW_TOKENS = 200000;
5
- function getMetricFromSessionRow(row, metric, refs = {}) {
6
- const cacheRow = refs.cacheBySessionId?.get(row.sessionId);
7
- const agentRow = refs.agentBySessionId?.get(row.sessionId);
8
- const turnDiffRows = refs.turnDiffBySessionId?.get(row.sessionId) ?? [];
9
- const contextRow = refs.contextBySessionId?.get(row.sessionId);
10
- switch (metric) {
11
- case 'events':
12
- return row.events ?? 0;
13
- case 'turns':
14
- return row.turns ?? 0;
15
- case 'usage.total':
16
- return row.usage?.total ?? 0;
17
- case 'usage.input':
18
- return row.usage?.input ?? 0;
19
- case 'usage.output':
20
- return row.usage?.output ?? 0;
21
- case 'cache.read':
22
- return cacheRow?.cacheReadInputTokens ?? 0;
23
- case 'cache.creation':
24
- return cacheRow?.cacheCreationInputTokens ?? 0;
25
- case 'cache.hit_rate':
26
- return cacheRow?.cacheHitRate ?? 0;
27
- case 'subagent.calls':
28
- return agentRow?.agentTasks ?? 0;
29
- case 'diff.char_delta.max':
30
- return turnDiffRows.reduce((max, rowItem) => Math.max(max, Math.abs(Number(rowItem.charDelta ?? 0))), 0);
31
- case 'context.unknown_delta_share.window5':
32
- return contextRow?.unknownDeltaShareWindow5 ?? 0;
33
- case 'context.unknown_top_streak':
34
- return contextRow?.unknownTopStreak ?? 0;
35
- case 'context.high_pollution_source_streak':
36
- return contextRow?.highPollutionSourceStreak ?? 0;
37
- case 'context.fill_ratio.max':
38
- return contextRow?.maxFillRatio ?? 0;
39
- case 'context.delta_ratio.max':
40
- return contextRow?.maxDeltaRatio ?? 0;
41
- case 'context.compact.count':
42
- return contextRow?.compactCount ?? 0;
43
- default:
44
- return 0;
45
- }
46
- }
47
- function parseJsonObject(value) {
48
- if (typeof value !== 'string' || !value.trim())
49
- return null;
50
- try {
51
- const parsed = JSON.parse(value);
52
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
53
- return parsed;
54
- }
55
- catch {
56
- // ignore parse errors
57
- }
58
- return null;
59
- }
60
- function parseJsonArray(value) {
61
- if (typeof value !== 'string' || !value.trim())
62
- return [];
63
- try {
64
- const parsed = JSON.parse(value);
65
- if (Array.isArray(parsed))
66
- return parsed;
67
- }
68
- catch {
69
- // ignore parse errors
70
- }
71
- return [];
72
- }
73
- function summarizeContextForSession(events) {
74
- const snapshots = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_snapshot');
75
- const attributions = events.filter((event) => event?.attributes?.['agentic.context.event_type'] === 'context_source_attribution');
76
- const last5 = snapshots.slice(-5);
77
- let unknownTokens = 0;
78
- let totalTokens = 0;
79
- for (const snapshot of last5) {
80
- const composition = parseJsonObject(snapshot?.attributes?.['agentic.context.composition']);
81
- if (!composition)
82
- continue;
83
- for (const [kind, raw] of Object.entries(composition)) {
84
- const value = Number(raw);
85
- if (!Number.isFinite(value) || value <= 0)
86
- continue;
87
- totalTokens += value;
88
- if (kind === 'unknown')
89
- unknownTokens += value;
90
- }
91
- }
92
- const unknownDeltaShareWindow5 = totalTokens > 0 ? unknownTokens / totalTokens : 0;
93
- let unknownTopStreak = 0;
94
- let runningUnknown = 0;
95
- for (const snapshot of snapshots) {
96
- const topSources = parseJsonArray(snapshot?.attributes?.['agentic.context.top_sources']);
97
- const top = String(topSources[0] ?? '').trim();
98
- if (top === 'unknown') {
99
- runningUnknown += 1;
100
- if (runningUnknown > unknownTopStreak)
101
- unknownTopStreak = runningUnknown;
102
- }
103
- else {
104
- runningUnknown = 0;
105
- }
106
- }
107
- const turnOrder = [];
108
- const highByTurn = new Map();
109
- for (const event of attributions) {
110
- const attrs = event?.attributes ?? {};
111
- const turnId = String(event?.turnId ?? '');
112
- if (!turnId)
113
- continue;
114
- if (!highByTurn.has(turnId)) {
115
- highByTurn.set(turnId, []);
116
- turnOrder.push(turnId);
117
- }
118
- const sourceKind = String(attrs['agentic.context.source_kind'] ?? '').trim();
119
- const score = Number(attrs['agentic.context.pollution_score']);
120
- if (!sourceKind || !Number.isFinite(score))
121
- continue;
122
- if (score < 80)
123
- continue;
124
- highByTurn.get(turnId).push({ sourceKind, score });
125
- }
126
- let highPollutionSourceStreak = 0;
127
- let runningSource = '';
128
- let runningCount = 0;
129
- for (const turnId of turnOrder) {
130
- const items = highByTurn.get(turnId) ?? [];
131
- if (!items.length) {
132
- runningSource = '';
133
- runningCount = 0;
134
- continue;
135
- }
136
- items.sort((a, b) => b.score - a.score);
137
- const topSource = items[0].sourceKind;
138
- if (topSource === runningSource) {
139
- runningCount += 1;
140
- }
141
- else {
142
- runningSource = topSource;
143
- runningCount = 1;
144
- }
145
- if (runningCount > highPollutionSourceStreak) {
146
- highPollutionSourceStreak = runningCount;
147
- }
148
- }
149
- let maxFillRatio = 0;
150
- let maxDeltaRatio = 0;
151
- let compactCount = 0;
152
- for (const event of events) {
153
- const attrs = event?.attributes ?? {};
154
- if (String(attrs['agentic.context.event_type'] ?? '') === 'context_snapshot') {
155
- const fillRatio = Number(attrs['agentic.context.fill_ratio']);
156
- if (Number.isFinite(fillRatio) && fillRatio > maxFillRatio)
157
- maxFillRatio = fillRatio;
158
- const deltaTokens = Number(attrs['agentic.context.delta_tokens']);
159
- if (Number.isFinite(deltaTokens) && deltaTokens > 0) {
160
- const ratio = deltaTokens / DEFAULT_CONTEXT_WINDOW_TOKENS;
161
- if (ratio > maxDeltaRatio)
162
- maxDeltaRatio = ratio;
163
- }
164
- }
165
- if (String(attrs['agentic.context.event_type'] ?? '') === 'context_boundary'
166
- && String(attrs['agentic.context.boundary_kind'] ?? '') === 'compact_after') {
167
- compactCount += 1;
168
- }
169
- }
170
- return {
171
- unknownDeltaShareWindow5,
172
- unknownTopStreak,
173
- highPollutionSourceStreak,
174
- maxFillRatio,
175
- maxDeltaRatio,
176
- compactCount,
177
- };
178
- }
179
- function getMetricFromAgentRow(row, metric) {
180
- switch (metric) {
181
- case 'agentTasks':
182
- return row.agentTasks ?? 0;
183
- case 'shellCommands':
184
- return row.shellCommands ?? 0;
185
- case 'mcpCalls':
186
- return row.mcpCalls ?? 0;
187
- case 'usage.total':
188
- return row.usage?.total ?? 0;
189
- default:
190
- return 0;
191
- }
192
- }
193
- function compare(value, op, threshold) {
194
- if (op === 'gt')
195
- return value > threshold;
196
- if (op === 'gte')
197
- return value >= threshold;
198
- if (op === 'lt')
199
- return value < threshold;
200
- if (op === 'lte')
201
- return value <= threshold;
202
- if (op === 'eq')
203
- return value === threshold;
204
- throw new Error(`unsupported operator: ${op}`);
205
- }
206
- function evaluateSessionRule(rule, sessions) {
207
- const rows = summarizeSessions(sessions);
208
- const cacheBySessionId = new Map(summarizeCache(sessions).map((row) => [row.sessionId, row]));
209
- const agentBySessionId = new Map(summarizeAgents(sessions).map((row) => [row.sessionId, row]));
210
- const turnDiffBySessionId = new Map();
211
- for (const row of summarizeTurnDiff(sessions)) {
212
- const key = row.sessionId;
213
- const list = turnDiffBySessionId.get(key) ?? [];
214
- list.push(row);
215
- turnDiffBySessionId.set(key, list);
216
- }
217
- const contextBySessionId = new Map(sessions.map((session) => [
218
- session.context?.sessionId ?? session.events?.[0]?.sessionId,
219
- summarizeContextForSession(session.events ?? []),
220
- ]));
221
- const matched = rows
222
- .map((row) => {
223
- const value = getMetricFromSessionRow(row, rule.metric, {
224
- cacheBySessionId,
225
- agentBySessionId,
226
- turnDiffBySessionId,
227
- contextBySessionId,
228
- });
229
- return { row, value };
230
- })
231
- .filter((x) => compare(x.value, rule.op, Number(rule.threshold)));
232
- return matched.map((m) => ({
233
- ruleId: rule.id,
234
- severity: rule.severity ?? 'warning',
235
- scope: 'session',
236
- metric: rule.metric,
237
- op: rule.op,
238
- threshold: Number(rule.threshold),
239
- value: m.value,
240
- context: {
241
- sessionId: m.row.sessionId,
242
- projectId: m.row.projectId,
243
- runtime: m.row.runtime,
244
- },
245
- }));
246
- }
247
- function evaluateAgentRule(rule, sessions) {
248
- const rows = summarizeAgents(sessions);
249
- const matched = rows
250
- .map((row) => {
251
- const value = getMetricFromAgentRow(row, rule.metric);
252
- return { row, value };
253
- })
254
- .filter((x) => compare(x.value, rule.op, Number(rule.threshold)));
255
- return matched.map((m) => ({
256
- ruleId: rule.id,
257
- severity: rule.severity ?? 'warning',
258
- scope: 'agent',
259
- metric: rule.metric,
260
- op: rule.op,
261
- threshold: Number(rule.threshold),
262
- value: m.value,
263
- context: {
264
- sessionId: m.row.sessionId,
265
- projectId: m.row.projectId,
266
- runtime: m.row.runtime,
267
- },
268
- }));
269
- }
270
- function evaluateMcpRule(rule, sessions) {
271
- const rows = summarizeMcp(sessions);
272
- const matched = rows.filter((row) => compare(row.calls ?? 0, rule.op, Number(rule.threshold)));
273
- return matched.map((row) => ({
274
- ruleId: rule.id,
275
- severity: rule.severity ?? 'warning',
276
- scope: 'mcp',
277
- metric: 'calls',
278
- op: rule.op,
279
- threshold: Number(rule.threshold),
280
- value: row.calls,
281
- context: {
282
- server: row.server,
283
- },
284
- }));
285
- }
286
- function evaluateCommandRule(rule, sessions) {
287
- const rows = summarizeCommands(sessions);
288
- const matched = rows.filter((row) => compare(row.calls ?? 0, rule.op, Number(rule.threshold)));
289
- return matched.map((row) => ({
290
- ruleId: rule.id,
291
- severity: rule.severity ?? 'warning',
292
- scope: 'command',
293
- metric: 'calls',
294
- op: rule.op,
295
- threshold: Number(rule.threshold),
296
- value: row.calls,
297
- context: {
298
- command: row.command,
299
- },
300
- }));
301
- }
302
- export async function loadAlertRules(path) {
303
- const raw = await readFile(path, 'utf-8');
304
- const parsed = JSON.parse(raw);
305
- if (!Array.isArray(parsed.rules)) {
306
- throw new Error('rule file must be JSON with {"rules": [...]}');
307
- }
308
- return parsed.rules;
309
- }
310
- export function evaluateRules(rules, sessions) {
311
- const alerts = [];
312
- for (const rule of rules) {
313
- if (!rule.id || !rule.scope || !rule.metric || !rule.op) {
314
- throw new Error(`invalid rule: ${JSON.stringify(rule)}`);
315
- }
316
- if (rule.scope === 'session') {
317
- alerts.push(...evaluateSessionRule(rule, sessions));
318
- continue;
319
- }
320
- if (rule.scope === 'agent') {
321
- alerts.push(...evaluateAgentRule(rule, sessions));
322
- continue;
323
- }
324
- if (rule.scope === 'mcp') {
325
- alerts.push(...evaluateMcpRule(rule, sessions));
326
- continue;
327
- }
328
- if (rule.scope === 'command') {
329
- alerts.push(...evaluateCommandRule(rule, sessions));
330
- continue;
331
- }
332
- throw new Error(`unsupported rule scope: ${rule.scope}`);
333
- }
334
- return alerts;
335
- }
336
- export async function sendAlertWebhook(url, payload, headers = {}) {
337
- const response = await fetch(url, {
338
- method: 'POST',
339
- headers: {
340
- 'content-type': 'application/json',
341
- ...headers,
342
- },
343
- body: JSON.stringify(payload),
344
- });
345
- if (!response.ok) {
346
- throw new Error(`webhook HTTP ${response.status}`);
347
- }
348
- }
package/dist/env.d.ts DELETED
@@ -1,6 +0,0 @@
1
- export declare function resolveUserHome(): string;
2
- export declare function resolveSpanoryHome(): string;
3
- export declare function resolveSpanoryEnvPath(): string;
4
- export declare function resolveLegacyUserEnvPath(): string;
5
- export declare function parseSimpleDotEnv(raw: any): {};
6
- export declare function loadUserEnv(): Promise<void>;
package/dist/env.js DELETED
@@ -1,82 +0,0 @@
1
- // @ts-nocheck
2
- import path from 'node:path';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
- export function resolveUserHome() {
5
- return process.env.HOME || process.env.USERPROFILE || '';
6
- }
7
- export function resolveSpanoryHome() {
8
- if (process.env.SPANORY_HOME)
9
- return process.env.SPANORY_HOME;
10
- const home = resolveUserHome();
11
- return home ? path.join(home, '.spanory') : '';
12
- }
13
- export function resolveSpanoryEnvPath() {
14
- const root = resolveSpanoryHome();
15
- return root ? path.join(root, '.env') : '';
16
- }
17
- export function resolveLegacyUserEnvPath() {
18
- const home = resolveUserHome();
19
- return home ? path.join(home, '.env') : '';
20
- }
21
- export function parseSimpleDotEnv(raw) {
22
- const out = {};
23
- for (const line of String(raw).split('\n')) {
24
- let s = line.trim();
25
- if (!s || s.startsWith('#'))
26
- continue;
27
- if (s.startsWith('export ')) {
28
- s = s.slice('export '.length).trim();
29
- }
30
- const idx = s.indexOf('=');
31
- if (idx <= 0)
32
- continue;
33
- const key = s.slice(0, idx).trim();
34
- if (!key)
35
- continue;
36
- let value = s.slice(idx + 1).trim();
37
- const quoted = (value.startsWith('"') && value.endsWith('"'))
38
- || (value.startsWith("'") && value.endsWith("'"));
39
- if (quoted) {
40
- value = value.slice(1, -1);
41
- }
42
- else {
43
- const commentIdx = value.indexOf(' #');
44
- if (commentIdx >= 0) {
45
- value = value.slice(0, commentIdx).trimEnd();
46
- }
47
- }
48
- out[key] = value;
49
- }
50
- return out;
51
- }
52
- export async function loadUserEnv() {
53
- const spanoryHome = resolveSpanoryHome();
54
- const envPath = resolveSpanoryEnvPath();
55
- const legacyEnvPath = resolveLegacyUserEnvPath();
56
- if (!spanoryHome || !envPath)
57
- return;
58
- try {
59
- await mkdir(spanoryHome, { recursive: true });
60
- await writeFile(envPath, '', { flag: 'a' });
61
- }
62
- catch {
63
- // best effort only
64
- }
65
- const candidates = [envPath, legacyEnvPath].filter(Boolean);
66
- for (const candidate of candidates) {
67
- try {
68
- const raw = await readFile(candidate, 'utf-8');
69
- const parsed = parseSimpleDotEnv(raw);
70
- if (Object.keys(parsed).length === 0)
71
- continue;
72
- for (const [k, v] of Object.entries(parsed)) {
73
- if (process.env[k] === undefined)
74
- process.env[k] = v;
75
- }
76
- return;
77
- }
78
- catch {
79
- // try next candidate
80
- }
81
- }
82
- }
@@ -1,39 +0,0 @@
1
- export declare const ISSUE_STATUSES: readonly ["open", "in_progress", "blocked", "done"];
2
- export type IssueStatus = (typeof ISSUE_STATUSES)[number];
3
- export interface IssueItem {
4
- id: string;
5
- title: string;
6
- source: string;
7
- status: IssueStatus;
8
- notes: string[];
9
- createdAt: string;
10
- updatedAt: string;
11
- closedAt?: string;
12
- }
13
- export interface IssueState {
14
- version: 1;
15
- updatedAt: string;
16
- issues: IssueItem[];
17
- }
18
- export interface PendingTodoItem {
19
- id: string;
20
- title: string;
21
- source: string;
22
- }
23
- export declare function parsePendingTodoItems(todoContent: string, source?: string): PendingTodoItem[];
24
- export declare function syncIssueState(prev: IssueState, pending: PendingTodoItem[], now?: string): {
25
- state: IssueState;
26
- added: number;
27
- reopened: number;
28
- autoClosed: number;
29
- };
30
- export declare function setIssueStatus(prev: IssueState, input: {
31
- id: string;
32
- status: string;
33
- note?: string;
34
- }, now?: string): IssueState;
35
- export declare function createEmptyIssueState(now?: string): IssueState;
36
- export declare function loadIssueState(filePath: string): Promise<IssueState>;
37
- export declare function saveIssueState(filePath: string, state: IssueState): Promise<void>;
38
- export declare function resolveIssueStatePath(input?: string): string;
39
- export declare function resolveTodoPath(input?: string): string;
@@ -1,151 +0,0 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- export const ISSUE_STATUSES = ['open', 'in_progress', 'blocked', 'done'];
4
- function nowIso(now) {
5
- return now ?? new Date().toISOString();
6
- }
7
- function normalizeId(id) {
8
- return String(id ?? '').trim();
9
- }
10
- function assertStatus(status) {
11
- if (!ISSUE_STATUSES.includes(status)) {
12
- throw new Error(`unsupported issue status: ${status}`);
13
- }
14
- }
15
- function assertAllowedTransition(from, to) {
16
- if (from === to)
17
- return;
18
- if (from === 'done' && to !== 'done') {
19
- throw new Error('cannot transition issue from done to non-done status');
20
- }
21
- }
22
- export function parsePendingTodoItems(todoContent, source = 'todo.md') {
23
- const lines = String(todoContent ?? '').split(/\r?\n/);
24
- const items = [];
25
- for (const line of lines) {
26
- const m = line.match(/^\s*-\s*\[\s\]\s+([A-Za-z0-9_-]+)\s+(.*)$/);
27
- if (!m)
28
- continue;
29
- items.push({
30
- id: normalizeId(m[1]),
31
- title: m[2].trim(),
32
- source,
33
- });
34
- }
35
- return items;
36
- }
37
- export function syncIssueState(prev, pending, now) {
38
- const timestamp = nowIso(now);
39
- const byId = new Map(prev.issues.map((issue) => [issue.id, issue]));
40
- const activeIds = new Set(pending.map((item) => item.id));
41
- let added = 0;
42
- let reopened = 0;
43
- let autoClosed = 0;
44
- for (const item of pending) {
45
- const existing = byId.get(item.id);
46
- if (!existing) {
47
- byId.set(item.id, {
48
- id: item.id,
49
- title: item.title,
50
- source: item.source,
51
- status: 'open',
52
- notes: ['synced from todo pending item'],
53
- createdAt: timestamp,
54
- updatedAt: timestamp,
55
- });
56
- added += 1;
57
- continue;
58
- }
59
- existing.title = item.title;
60
- existing.source = item.source;
61
- existing.updatedAt = timestamp;
62
- if (existing.status === 'done') {
63
- existing.status = 'open';
64
- existing.closedAt = undefined;
65
- existing.notes.push('reopened by todo pending item');
66
- reopened += 1;
67
- }
68
- }
69
- for (const issue of byId.values()) {
70
- if (issue.source !== 'todo.md')
71
- continue;
72
- if (activeIds.has(issue.id))
73
- continue;
74
- if (issue.status === 'done')
75
- continue;
76
- issue.status = 'done';
77
- issue.updatedAt = timestamp;
78
- issue.closedAt = timestamp;
79
- issue.notes.push('auto-closed because todo item is no longer pending');
80
- autoClosed += 1;
81
- }
82
- return {
83
- state: {
84
- version: 1,
85
- updatedAt: timestamp,
86
- issues: Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id)),
87
- },
88
- added,
89
- reopened,
90
- autoClosed,
91
- };
92
- }
93
- export function setIssueStatus(prev, input, now) {
94
- const issueId = normalizeId(input.id);
95
- const timestamp = nowIso(now);
96
- assertStatus(input.status);
97
- const issue = prev.issues.find((item) => item.id === issueId);
98
- if (!issue)
99
- throw new Error(`issue not found: ${issueId}`);
100
- assertAllowedTransition(issue.status, input.status);
101
- issue.status = input.status;
102
- issue.updatedAt = timestamp;
103
- if (input.status === 'done')
104
- issue.closedAt = timestamp;
105
- if (input.status !== 'done')
106
- issue.closedAt = undefined;
107
- if (input.note && input.note.trim())
108
- issue.notes.push(input.note.trim());
109
- return {
110
- version: 1,
111
- updatedAt: timestamp,
112
- issues: prev.issues,
113
- };
114
- }
115
- export function createEmptyIssueState(now) {
116
- return {
117
- version: 1,
118
- updatedAt: nowIso(now),
119
- issues: [],
120
- };
121
- }
122
- export async function loadIssueState(filePath) {
123
- try {
124
- const raw = await readFile(filePath, 'utf-8');
125
- const parsed = JSON.parse(raw);
126
- if (!parsed || !Array.isArray(parsed.issues)) {
127
- throw new Error('invalid issue state file');
128
- }
129
- return {
130
- version: 1,
131
- updatedAt: String(parsed.updatedAt ?? new Date(0).toISOString()),
132
- issues: parsed.issues,
133
- };
134
- }
135
- catch (error) {
136
- if (error?.code === 'ENOENT') {
137
- return createEmptyIssueState();
138
- }
139
- throw error;
140
- }
141
- }
142
- export async function saveIssueState(filePath, state) {
143
- await mkdir(path.dirname(filePath), { recursive: true });
144
- await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
145
- }
146
- export function resolveIssueStatePath(input) {
147
- return input ? path.resolve(process.cwd(), input) : path.resolve(process.cwd(), 'docs/issues/state.json');
148
- }
149
- export function resolveTodoPath(input) {
150
- return input ? path.resolve(process.cwd(), input) : path.resolve(process.cwd(), 'todo.md');
151
- }
package/dist/otlp.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import { buildResource } from '../../otlp-core/dist/index.js';
2
- export { buildResource };
3
- export declare function parseHeaders(input: any): Record<string, string>;
4
- export declare function compileOtlp(events: any, resource: any): import("../../otlp-core/dist/index.js").OtlpPayload;
5
- export declare function sendOtlp(endpoint: any, payload: any, headers?: {}): Promise<void>;
package/dist/otlp.js DELETED
@@ -1,12 +0,0 @@
1
- // @ts-nocheck
2
- import { buildResource, compileOtlpSpans, parseOtlpHeaders, sendOtlpHttp, } from '../../otlp-core/dist/index.js';
3
- export { buildResource };
4
- export function parseHeaders(input) {
5
- return parseOtlpHeaders(input);
6
- }
7
- export function compileOtlp(events, resource) {
8
- return compileOtlpSpans(events, resource);
9
- }
10
- export async function sendOtlp(endpoint, payload, headers = {}) {
11
- await sendOtlpHttp(endpoint, payload, headers);
12
- }