@basou/sdk 0.5.0 → 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.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,163 @@
1
- declare const BASOU_SDK_VERSION = "0.1.0";
1
+ import { Manifest, StatusSnapshot, SessionEntry, Event, TaskDocument, LoadedApproval, WorkStatsResult } from '@basou/core';
2
+ export { ActiveTimeBasis, Approval, ApprovalStatus, CommandExecutedEvent, DayWorkStats, DecisionRecordedEvent, Event, FileChangedEvent, LoadedApproval, Manifest, MeasureAvailability, NoteAddedEvent, RiskLevel, Session, SessionEndedEvent, SessionEntry, SessionMetrics, SessionSourceKind, SessionStartedEvent, SessionStatus, SessionStatusChangedEvent, SessionWorkStats, SourceWorkStats, StatusCount, StatusSnapshot, SuspectReason, Task, TaskDocument, TaskStatus, TokenTotals, WorkStatsResult, WorkStatsTotals } from '@basou/core';
2
3
 
3
- export { BASOU_SDK_VERSION };
4
+ /**
5
+ * Base class for every error the SDK throws on its own behalf. Errors that
6
+ * originate in `@basou/core` (e.g. a malformed `session.yaml`) propagate as-is;
7
+ * only the SDK's own preconditions are wrapped, so `instanceof BasouSdkError`
8
+ * identifies "the SDK rejected this call" rather than "the data was bad".
9
+ */
10
+ declare class BasouSdkError extends Error {
11
+ constructor(message: string, options?: {
12
+ cause?: unknown;
13
+ });
14
+ }
15
+ /**
16
+ * `openWorkspace` was pointed at a path that is not a usable Basou workspace:
17
+ * the `.basou/` directory is missing, is a symlink, or is otherwise not a
18
+ * directory. The offending repository root is on {@link root}.
19
+ */
20
+ declare class WorkspaceNotFoundError extends BasouSdkError {
21
+ readonly root: string;
22
+ constructor(root: string, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /**
27
+ * A session / task id prefix matched more than one record. The {@link input}
28
+ * is the prefix as given; the caller should retry with a longer one. (A prefix
29
+ * that matches nothing is NOT an error — the lookup returns `null` instead.)
30
+ */
31
+ declare class AmbiguousIdError extends BasouSdkError {
32
+ readonly input: string;
33
+ constructor(input: string, options?: {
34
+ cause?: unknown;
35
+ });
36
+ }
37
+
38
+ /**
39
+ * A degradation the SDK noticed while reading provenance: a malformed event
40
+ * line, or a session / task that could not be loaded. Best-effort reads skip
41
+ * these and keep going; pass `onDiagnostic` to {@link openWorkspace} to observe
42
+ * them. `message` is a human-readable summary (it folds in the core
43
+ * `ReplayWarning.kind` or skip-reason); structured fields are intentionally not
44
+ * part of this stable shape.
45
+ */
46
+ type WorkspaceDiagnostic = {
47
+ /** Human-readable summary of the malformed line / skipped record. */
48
+ message: string;
49
+ /** Session or task id the diagnostic relates to, when known. */
50
+ id?: string;
51
+ };
52
+ /** Options for {@link openWorkspace}; all optional. */
53
+ type WorkspaceOptions = {
54
+ /**
55
+ * Clock used for time-sensitive reads (session "suspect" classification,
56
+ * stats span-to-now, status / approval expiry). Injectable for deterministic
57
+ * callers and tests. Defaults to `() => new Date()`, evaluated per call.
58
+ */
59
+ now?: () => Date;
60
+ /**
61
+ * Observe a malformed event line or a skipped session / task instead of it
62
+ * being silently dropped. Reads are still best-effort: a diagnostic does not
63
+ * fail the call.
64
+ */
65
+ onDiagnostic?: (diagnostic: WorkspaceDiagnostic) => void;
66
+ };
67
+ /** Options for {@link Workspace.stats}. */
68
+ type StatsOptions = {
69
+ /**
70
+ * IANA timezone used to bucket the per-day breakdown (native logs are UTC).
71
+ * Defaults to the host's local zone.
72
+ */
73
+ timeZone?: string;
74
+ };
75
+ /**
76
+ * A read-only handle on one Basou workspace (`<root>/.basou/`). Every method
77
+ * reads provenance from disk; the SDK exposes no writers. Obtain one with
78
+ * {@link openWorkspace}.
79
+ *
80
+ * Session / task lookups (`getSession`, `getTask`, `readEvents`,
81
+ * `streamEvents`) accept a full id or a unique prefix: a prefix matching
82
+ * nothing yields `null` (or an empty stream), a prefix matching more than one
83
+ * record throws {@link AmbiguousIdError}. `getApproval` takes an exact id only.
84
+ */
85
+ interface Workspace {
86
+ /** Absolute repository root this workspace was opened at. */
87
+ readonly root: string;
88
+ /** Parsed `manifest.yaml`. */
89
+ manifest(): Promise<Manifest>;
90
+ /** A freshly computed workspace status snapshot (directory presence + manifest). */
91
+ status(): Promise<StatusSnapshot>;
92
+ /** Every session, ULID-ascending, each with its `suspect` classification. */
93
+ listSessions(): Promise<SessionEntry[]>;
94
+ /** One session by id / unique prefix, or `null` if no session matches. */
95
+ getSession(idOrPrefix: string): Promise<SessionEntry | null>;
96
+ /** All events of a session, eagerly, ordered as written. Empty if no match. */
97
+ readEvents(idOrPrefix: string): Promise<Event[]>;
98
+ /** All events of a session as a lazy stream (for large logs). */
99
+ streamEvents(idOrPrefix: string): AsyncIterable<Event>;
100
+ /** Every task (active + lazily-indexed), created-at ascending. */
101
+ listTasks(): Promise<TaskDocument[]>;
102
+ /** One task by id / unique prefix (archived included), or `null`. */
103
+ getTask(idOrPrefix: string): Promise<TaskDocument | null>;
104
+ /** Pending + resolved approvals, fully loaded. */
105
+ listApprovals(): Promise<{
106
+ pending: LoadedApproval[];
107
+ resolved: LoadedApproval[];
108
+ }>;
109
+ /** One approval by exact id (resolved checked first), or `null`. */
110
+ getApproval(id: string): Promise<LoadedApproval | null>;
111
+ /** Aggregated work / time / token stats across the workspace's sessions. */
112
+ stats(options?: StatsOptions): Promise<WorkStatsResult>;
113
+ /** The rendered `handoff.md` body (recomputed, without generated markers). */
114
+ renderHandoff(): Promise<string>;
115
+ /** The rendered `decisions.md` body (recomputed, without generated markers). */
116
+ renderDecisions(): Promise<string>;
117
+ }
118
+ /**
119
+ * Resolve the Basou workspace root for a working directory by finding the
120
+ * enclosing git repository root (`.basou/` lives at the repo root). A
121
+ * convenience for the common "I'm somewhere in the repo" case; requires git
122
+ * and a repository. Pass the returned path to {@link openWorkspace}. When you
123
+ * already know the root (CI checkout, a copied `.basou/`), skip this and call
124
+ * {@link openWorkspace} directly — it needs no git.
125
+ */
126
+ declare function resolveWorkspaceRoot(cwd: string): Promise<string>;
127
+ /**
128
+ * Open a read-only handle on the Basou workspace rooted at `repoRoot` (the
129
+ * directory that contains `.basou/`). Validates that `.basou/` exists and is a
130
+ * real directory; throws {@link WorkspaceNotFoundError} otherwise. No git is
131
+ * required — point it at any directory holding a `.basou/`.
132
+ */
133
+ declare function openWorkspace(repoRoot: string, options?: WorkspaceOptions): Promise<Workspace>;
134
+
135
+ /**
136
+ * `@basou/sdk` — the stable, read-only programmatic API for reading a Basou
137
+ * workspace's provenance (`.basou/`). It is a thin, ergonomic facade over
138
+ * `@basou/core`'s readers: open a workspace once and query sessions, events,
139
+ * tasks, approvals, status, stats, and the rendered handoff / decisions. No
140
+ * writers are exposed — third-party tooling can read provenance without any
141
+ * risk of mutating it.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * import { openWorkspace, resolveWorkspaceRoot } from "@basou/sdk";
146
+ *
147
+ * const root = await resolveWorkspaceRoot(process.cwd()); // or pass a known root
148
+ * const ws = await openWorkspace(root);
149
+ * for (const { session, suspect } of await ws.listSessions()) {
150
+ * console.log(session.session.id, session.session.status, suspect);
151
+ * }
152
+ * const stats = await ws.stats();
153
+ * console.log(stats.totals.billableActiveTimeMs);
154
+ * ```
155
+ */
156
+ /**
157
+ * SDK API version, tracking the Basou SDK surface (not the npm package
158
+ * version, which moves in lockstep with the monorepo). `0.2.0` is the first
159
+ * release with a runtime read API; `0.1.0` was types-only.
160
+ */
161
+ declare const BASOU_SDK_VERSION = "0.2.0";
162
+
163
+ export { AmbiguousIdError, BASOU_SDK_VERSION, BasouSdkError, type StatsOptions, type Workspace, type WorkspaceDiagnostic, WorkspaceNotFoundError, type WorkspaceOptions, openWorkspace, resolveWorkspaceRoot };
package/dist/index.js CHANGED
@@ -1,6 +1,174 @@
1
+ // src/errors.ts
2
+ var BasouSdkError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var WorkspaceNotFoundError = class extends BasouSdkError {
9
+ root;
10
+ constructor(root, options) {
11
+ super(
12
+ `No Basou workspace at ${root}: expected a '.basou/' directory (run 'basou init' there first).`,
13
+ options
14
+ );
15
+ this.root = root;
16
+ }
17
+ };
18
+ var AmbiguousIdError = class extends BasouSdkError {
19
+ input;
20
+ constructor(input, options) {
21
+ super(`Ambiguous id '${input}': matched more than one record; use a longer prefix.`, options);
22
+ this.input = input;
23
+ }
24
+ };
25
+
26
+ // src/workspace.ts
27
+ import { join, resolve } from "path";
28
+ import {
29
+ assertBasouRootSafe,
30
+ basouPaths,
31
+ buildStatusSnapshot,
32
+ computeWorkStats,
33
+ enumerateApprovals,
34
+ loadApproval,
35
+ loadSessionEntries,
36
+ loadTaskEntries,
37
+ readAllEvents,
38
+ readManifest,
39
+ readTaskFileWithArchiveFallback,
40
+ renderDecisions,
41
+ renderHandoff,
42
+ replayEvents,
43
+ resolveRepositoryRoot,
44
+ resolveSessionId,
45
+ resolveTaskId
46
+ } from "@basou/core";
47
+ function resolveWorkspaceRoot(cwd) {
48
+ return resolveRepositoryRoot(cwd);
49
+ }
50
+ async function openWorkspace(repoRoot, options = {}) {
51
+ const root = resolve(repoRoot);
52
+ const paths = basouPaths(root);
53
+ try {
54
+ await assertBasouRootSafe(paths.root);
55
+ } catch (cause) {
56
+ throw new WorkspaceNotFoundError(root, { cause });
57
+ }
58
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
59
+ const emit = options.onDiagnostic;
60
+ const onWarning = (warning, id) => emit?.({
61
+ message: `event ${warning.kind}${warning.line ? ` (line ${warning.line})` : ""}`,
62
+ ...id !== void 0 ? { id } : {}
63
+ });
64
+ const onSkip = (id, reason) => emit?.({ message: `skipped: ${reason}`, id });
65
+ const resolveSession = (input) => resolveOrNull(() => resolveSessionId(paths, input), input);
66
+ const resolveTask = (input) => resolveOrNull(() => resolveTaskId(paths, input, { includeArchived: true }), input);
67
+ return {
68
+ root,
69
+ manifest: () => readManifest(paths),
70
+ status: async () => buildStatusSnapshot({ manifest: await readManifest(paths), paths, now: now() }),
71
+ listSessions: () => loadSessionEntries(paths, {
72
+ now: now(),
73
+ onWarning: (w, sid) => onWarning(w, sid),
74
+ onSkip
75
+ }),
76
+ getSession: async (idOrPrefix) => {
77
+ const id = await resolveSession(idOrPrefix);
78
+ if (id === null) return null;
79
+ const entries = await loadSessionEntries(paths, {
80
+ now: now(),
81
+ onWarning: (w, sid) => onWarning(w, sid),
82
+ onSkip
83
+ });
84
+ return entries.find((e) => e.sessionId === id) ?? null;
85
+ },
86
+ readEvents: async (idOrPrefix) => {
87
+ const id = await resolveSession(idOrPrefix);
88
+ if (id === null) return [];
89
+ return readAllEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });
90
+ },
91
+ streamEvents: (idOrPrefix) => {
92
+ async function* iterate() {
93
+ const id = await resolveSession(idOrPrefix);
94
+ if (id === null) return;
95
+ yield* replayEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });
96
+ }
97
+ return iterate();
98
+ },
99
+ listTasks: () => loadTaskEntries(paths, { onSkip }),
100
+ getTask: async (idOrPrefix) => {
101
+ const id = await resolveTask(idOrPrefix);
102
+ if (id === null) return null;
103
+ const { doc } = await readTaskFileWithArchiveFallback(paths, id);
104
+ return doc;
105
+ },
106
+ listApprovals: async () => {
107
+ const ids = await enumerateApprovals(paths);
108
+ const resolvedSet = new Set(ids.resolved);
109
+ const pendingIds = ids.pending.filter((id) => !resolvedSet.has(id));
110
+ const load = async (id) => loadApproval(paths, id);
111
+ const [pending, resolved] = await Promise.all([
112
+ Promise.all(pendingIds.map(load)),
113
+ Promise.all(ids.resolved.map(load))
114
+ ]);
115
+ return {
116
+ pending: pending.filter((a) => a !== null),
117
+ resolved: resolved.filter((a) => a !== null)
118
+ };
119
+ },
120
+ getApproval: (id) => loadApproval(paths, id),
121
+ stats: (statsOptions) => computeWorkStats({
122
+ paths,
123
+ now: now(),
124
+ ...statsOptions?.timeZone !== void 0 ? { timeZone: statsOptions.timeZone } : {},
125
+ onWarning: (w, sid) => onWarning(w, sid),
126
+ onSessionSkip: onSkip
127
+ }),
128
+ renderHandoff: async () => {
129
+ const result = await renderHandoff({
130
+ paths,
131
+ nowIso: now().toISOString(),
132
+ onWarning: (w, sid) => onWarning(w, sid),
133
+ onSessionSkip: onSkip,
134
+ onTaskSkip: onSkip
135
+ });
136
+ return result.body;
137
+ },
138
+ renderDecisions: async () => {
139
+ const result = await renderDecisions({
140
+ paths,
141
+ nowIso: now().toISOString(),
142
+ onWarning: (w, sid) => onWarning(w, sid),
143
+ onSessionSkip: onSkip
144
+ });
145
+ return result.body;
146
+ }
147
+ };
148
+ }
149
+ async function resolveOrNull(resolver, input) {
150
+ try {
151
+ return await resolver();
152
+ } catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ if (/^Ambiguous (session|task) id /.test(message)) {
155
+ throw new AmbiguousIdError(input, { cause: error });
156
+ }
157
+ if (/^(Session|Task) not found: /.test(message) || /^(Session|Task) id is empty$/.test(message)) {
158
+ return null;
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+
1
164
  // src/index.ts
2
- var BASOU_SDK_VERSION = "0.1.0";
165
+ var BASOU_SDK_VERSION = "0.2.0";
3
166
  export {
4
- BASOU_SDK_VERSION
167
+ AmbiguousIdError,
168
+ BASOU_SDK_VERSION,
169
+ BasouSdkError,
170
+ WorkspaceNotFoundError,
171
+ openWorkspace,
172
+ resolveWorkspaceRoot
5
173
  };
6
174
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const BASOU_SDK_VERSION = \"0.1.0\";\n"],"mappings":";AAAO,IAAM,oBAAoB;","names":[]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/workspace.ts","../src/index.ts"],"sourcesContent":["/**\n * Base class for every error the SDK throws on its own behalf. Errors that\n * originate in `@basou/core` (e.g. a malformed `session.yaml`) propagate as-is;\n * only the SDK's own preconditions are wrapped, so `instanceof BasouSdkError`\n * identifies \"the SDK rejected this call\" rather than \"the data was bad\".\n */\nexport class BasouSdkError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/**\n * `openWorkspace` was pointed at a path that is not a usable Basou workspace:\n * the `.basou/` directory is missing, is a symlink, or is otherwise not a\n * directory. The offending repository root is on {@link root}.\n */\nexport class WorkspaceNotFoundError extends BasouSdkError {\n readonly root: string;\n constructor(root: string, options?: { cause?: unknown }) {\n super(\n `No Basou workspace at ${root}: expected a '.basou/' directory (run 'basou init' there first).`,\n options,\n );\n this.root = root;\n }\n}\n\n/**\n * A session / task id prefix matched more than one record. The {@link input}\n * is the prefix as given; the caller should retry with a longer one. (A prefix\n * that matches nothing is NOT an error — the lookup returns `null` instead.)\n */\nexport class AmbiguousIdError extends BasouSdkError {\n readonly input: string;\n constructor(input: string, options?: { cause?: unknown }) {\n super(`Ambiguous id '${input}': matched more than one record; use a longer prefix.`, options);\n this.input = input;\n }\n}\n","import { join, resolve } from \"node:path\";\nimport {\n assertBasouRootSafe,\n basouPaths,\n buildStatusSnapshot,\n computeWorkStats,\n type Event,\n enumerateApprovals,\n type LoadedApproval,\n loadApproval,\n loadSessionEntries,\n loadTaskEntries,\n type Manifest,\n readAllEvents,\n readManifest,\n readTaskFileWithArchiveFallback,\n renderDecisions,\n renderHandoff,\n replayEvents,\n resolveRepositoryRoot,\n resolveSessionId,\n resolveTaskId,\n type SessionEntry,\n type StatusSnapshot,\n type TaskDocument,\n type WorkStatsResult,\n} from \"@basou/core\";\nimport { AmbiguousIdError, WorkspaceNotFoundError } from \"./errors.js\";\n\n/**\n * A degradation the SDK noticed while reading provenance: a malformed event\n * line, or a session / task that could not be loaded. Best-effort reads skip\n * these and keep going; pass `onDiagnostic` to {@link openWorkspace} to observe\n * them. `message` is a human-readable summary (it folds in the core\n * `ReplayWarning.kind` or skip-reason); structured fields are intentionally not\n * part of this stable shape.\n */\nexport type WorkspaceDiagnostic = {\n /** Human-readable summary of the malformed line / skipped record. */\n message: string;\n /** Session or task id the diagnostic relates to, when known. */\n id?: string;\n};\n\n/** Options for {@link openWorkspace}; all optional. */\nexport type WorkspaceOptions = {\n /**\n * Clock used for time-sensitive reads (session \"suspect\" classification,\n * stats span-to-now, status / approval expiry). Injectable for deterministic\n * callers and tests. Defaults to `() => new Date()`, evaluated per call.\n */\n now?: () => Date;\n /**\n * Observe a malformed event line or a skipped session / task instead of it\n * being silently dropped. Reads are still best-effort: a diagnostic does not\n * fail the call.\n */\n onDiagnostic?: (diagnostic: WorkspaceDiagnostic) => void;\n};\n\n/** Options for {@link Workspace.stats}. */\nexport type StatsOptions = {\n /**\n * IANA timezone used to bucket the per-day breakdown (native logs are UTC).\n * Defaults to the host's local zone.\n */\n timeZone?: string;\n};\n\n/**\n * A read-only handle on one Basou workspace (`<root>/.basou/`). Every method\n * reads provenance from disk; the SDK exposes no writers. Obtain one with\n * {@link openWorkspace}.\n *\n * Session / task lookups (`getSession`, `getTask`, `readEvents`,\n * `streamEvents`) accept a full id or a unique prefix: a prefix matching\n * nothing yields `null` (or an empty stream), a prefix matching more than one\n * record throws {@link AmbiguousIdError}. `getApproval` takes an exact id only.\n */\nexport interface Workspace {\n /** Absolute repository root this workspace was opened at. */\n readonly root: string;\n\n /** Parsed `manifest.yaml`. */\n manifest(): Promise<Manifest>;\n /** A freshly computed workspace status snapshot (directory presence + manifest). */\n status(): Promise<StatusSnapshot>;\n\n /** Every session, ULID-ascending, each with its `suspect` classification. */\n listSessions(): Promise<SessionEntry[]>;\n /** One session by id / unique prefix, or `null` if no session matches. */\n getSession(idOrPrefix: string): Promise<SessionEntry | null>;\n /** All events of a session, eagerly, ordered as written. Empty if no match. */\n readEvents(idOrPrefix: string): Promise<Event[]>;\n /** All events of a session as a lazy stream (for large logs). */\n streamEvents(idOrPrefix: string): AsyncIterable<Event>;\n\n /** Every task (active + lazily-indexed), created-at ascending. */\n listTasks(): Promise<TaskDocument[]>;\n /** One task by id / unique prefix (archived included), or `null`. */\n getTask(idOrPrefix: string): Promise<TaskDocument | null>;\n\n /** Pending + resolved approvals, fully loaded. */\n listApprovals(): Promise<{ pending: LoadedApproval[]; resolved: LoadedApproval[] }>;\n /** One approval by exact id (resolved checked first), or `null`. */\n getApproval(id: string): Promise<LoadedApproval | null>;\n\n /** Aggregated work / time / token stats across the workspace's sessions. */\n stats(options?: StatsOptions): Promise<WorkStatsResult>;\n\n /** The rendered `handoff.md` body (recomputed, without generated markers). */\n renderHandoff(): Promise<string>;\n /** The rendered `decisions.md` body (recomputed, without generated markers). */\n renderDecisions(): Promise<string>;\n}\n\n/**\n * Resolve the Basou workspace root for a working directory by finding the\n * enclosing git repository root (`.basou/` lives at the repo root). A\n * convenience for the common \"I'm somewhere in the repo\" case; requires git\n * and a repository. Pass the returned path to {@link openWorkspace}. When you\n * already know the root (CI checkout, a copied `.basou/`), skip this and call\n * {@link openWorkspace} directly — it needs no git.\n */\nexport function resolveWorkspaceRoot(cwd: string): Promise<string> {\n return resolveRepositoryRoot(cwd);\n}\n\n/**\n * Open a read-only handle on the Basou workspace rooted at `repoRoot` (the\n * directory that contains `.basou/`). Validates that `.basou/` exists and is a\n * real directory; throws {@link WorkspaceNotFoundError} otherwise. No git is\n * required — point it at any directory holding a `.basou/`.\n */\nexport async function openWorkspace(\n repoRoot: string,\n options: WorkspaceOptions = {},\n): Promise<Workspace> {\n // Normalize to an absolute path up front so `root` honors its documented\n // absolute-path contract even when the caller passes a relative directory.\n const root = resolve(repoRoot);\n const paths = basouPaths(root);\n try {\n await assertBasouRootSafe(paths.root);\n } catch (cause) {\n throw new WorkspaceNotFoundError(root, { cause });\n }\n const now = options.now ?? (() => new Date());\n const emit = options.onDiagnostic;\n const onWarning = (warning: { kind: string; line?: number }, id?: string): void =>\n emit?.({\n message: `event ${warning.kind}${warning.line ? ` (line ${warning.line})` : \"\"}`,\n ...(id !== undefined ? { id } : {}),\n });\n const onSkip = (id: string, reason: string): void =>\n emit?.({ message: `skipped: ${reason}`, id });\n\n /** Resolve a session prefix to a full id, or null when nothing matches. */\n const resolveSession = (input: string): Promise<string | null> =>\n resolveOrNull(() => resolveSessionId(paths, input), input);\n const resolveTask = (input: string): Promise<string | null> =>\n resolveOrNull(() => resolveTaskId(paths, input, { includeArchived: true }), input);\n\n return {\n root,\n\n manifest: () => readManifest(paths),\n\n status: async () =>\n buildStatusSnapshot({ manifest: await readManifest(paths), paths, now: now() }),\n\n listSessions: () =>\n loadSessionEntries(paths, {\n now: now(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSkip,\n }),\n\n getSession: async (idOrPrefix) => {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return null;\n const entries = await loadSessionEntries(paths, {\n now: now(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSkip,\n });\n return entries.find((e) => e.sessionId === id) ?? null;\n },\n\n readEvents: async (idOrPrefix) => {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return [];\n return readAllEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });\n },\n\n streamEvents: (idOrPrefix): AsyncIterable<Event> => {\n async function* iterate(): AsyncGenerator<Event> {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return;\n yield* replayEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });\n }\n return iterate();\n },\n\n listTasks: () => loadTaskEntries(paths, { onSkip }),\n\n getTask: async (idOrPrefix) => {\n const id = await resolveTask(idOrPrefix);\n if (id === null) return null;\n const { doc } = await readTaskFileWithArchiveFallback(paths, id);\n return doc;\n },\n\n listApprovals: async () => {\n const ids = await enumerateApprovals(paths);\n // `loadApproval` searches resolved/ before pending/, so an id present in\n // BOTH (a stale pending file left after resolution) would otherwise load\n // the resolved record into the pending list too. Drop those from pending\n // so a resolved approval is reported once, under `resolved`.\n const resolvedSet = new Set(ids.resolved);\n const pendingIds = ids.pending.filter((id) => !resolvedSet.has(id));\n const load = async (id: string): Promise<LoadedApproval | null> => loadApproval(paths, id);\n const [pending, resolved] = await Promise.all([\n Promise.all(pendingIds.map(load)),\n Promise.all(ids.resolved.map(load)),\n ]);\n return {\n pending: pending.filter((a): a is LoadedApproval => a !== null),\n resolved: resolved.filter((a): a is LoadedApproval => a !== null),\n };\n },\n\n getApproval: (id) => loadApproval(paths, id),\n\n stats: (statsOptions) =>\n computeWorkStats({\n paths,\n now: now(),\n ...(statsOptions?.timeZone !== undefined ? { timeZone: statsOptions.timeZone } : {}),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n }),\n\n renderHandoff: async () => {\n const result = await renderHandoff({\n paths,\n nowIso: now().toISOString(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n onTaskSkip: onSkip,\n });\n return result.body;\n },\n\n renderDecisions: async () => {\n const result = await renderDecisions({\n paths,\n nowIso: now().toISOString(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n });\n return result.body;\n },\n };\n}\n\n/**\n * Run a core id-resolver and normalize its outcome: a successful resolution\n * returns the id; the \"not found\" / \"empty input\" contract errors map to\n * `null` (no such record); the \"ambiguous\" contract error maps to\n * {@link AmbiguousIdError}. Any other error propagates unchanged.\n */\nasync function resolveOrNull(\n resolver: () => Promise<string>,\n input: string,\n): Promise<string | null> {\n try {\n return await resolver();\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Match the core resolver's exact contract strings (id-resolver.ts), not a\n // loose substring, so an unrelated error that merely contains \"not found\"\n // is never silently swallowed to null.\n if (/^Ambiguous (session|task) id /.test(message)) {\n throw new AmbiguousIdError(input, { cause: error });\n }\n if (\n /^(Session|Task) not found: /.test(message) ||\n /^(Session|Task) id is empty$/.test(message)\n ) {\n return null;\n }\n throw error;\n }\n}\n","/**\n * `@basou/sdk` — the stable, read-only programmatic API for reading a Basou\n * workspace's provenance (`.basou/`). It is a thin, ergonomic facade over\n * `@basou/core`'s readers: open a workspace once and query sessions, events,\n * tasks, approvals, status, stats, and the rendered handoff / decisions. No\n * writers are exposed — third-party tooling can read provenance without any\n * risk of mutating it.\n *\n * @example\n * ```ts\n * import { openWorkspace, resolveWorkspaceRoot } from \"@basou/sdk\";\n *\n * const root = await resolveWorkspaceRoot(process.cwd()); // or pass a known root\n * const ws = await openWorkspace(root);\n * for (const { session, suspect } of await ws.listSessions()) {\n * console.log(session.session.id, session.session.status, suspect);\n * }\n * const stats = await ws.stats();\n * console.log(stats.totals.billableActiveTimeMs);\n * ```\n */\n\n/**\n * SDK API version, tracking the Basou SDK surface (not the npm package\n * version, which moves in lockstep with the monorepo). `0.2.0` is the first\n * release with a runtime read API; `0.1.0` was types-only.\n */\nexport const BASOU_SDK_VERSION = \"0.2.0\";\n\n// Read types re-exported from @basou/core so consumers can type the values the\n// SDK returns without depending on @basou/core directly. These track the\n// on-disk provenance schema.\nexport type {\n ActiveTimeBasis,\n Approval,\n ApprovalStatus,\n CommandExecutedEvent,\n DayWorkStats,\n DecisionRecordedEvent,\n Event,\n FileChangedEvent,\n LoadedApproval,\n Manifest,\n MeasureAvailability,\n NoteAddedEvent,\n RiskLevel,\n Session,\n SessionEndedEvent,\n SessionEntry,\n SessionMetrics,\n SessionSourceKind,\n SessionStartedEvent,\n SessionStatus,\n SessionStatusChangedEvent,\n SessionWorkStats,\n SourceWorkStats,\n StatusCount,\n StatusSnapshot,\n SuspectReason,\n Task,\n TaskDocument,\n TaskStatus,\n TokenTotals,\n WorkStatsResult,\n WorkStatsTotals,\n} from \"@basou/core\";\nexport { AmbiguousIdError, BasouSdkError, WorkspaceNotFoundError } from \"./errors.js\";\nexport {\n openWorkspace,\n resolveWorkspaceRoot,\n type StatsOptions,\n type Workspace,\n type WorkspaceDiagnostic,\n type WorkspaceOptions,\n} from \"./workspace.js\";\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@basou/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
- "description": "Type-only SDK for Basou adapter authors. v0.1: types only, no runtime.",
6
+ "description": "Read-only SDK for Basou: a stable, ergonomic API to read a workspace's provenance (sessions, events, tasks, approvals, stats).",
7
7
  "author": "Takashi Matsuyama",
8
8
  "keywords": [
9
9
  "ai",
10
10
  "agent",
11
11
  "sdk",
12
- "adapter",
12
+ "provenance",
13
13
  "basou"
14
14
  ],
15
15
  "repository": {
@@ -41,6 +41,9 @@
41
41
  "engines": {
42
42
  "node": ">=20.10.0"
43
43
  },
44
+ "dependencies": {
45
+ "@basou/core": "0.6.0"
46
+ },
44
47
  "scripts": {
45
48
  "build": "tsup",
46
49
  "dev": "tsup --watch",