@mulmoclaude/core 0.23.0 → 0.23.1

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.
@@ -854,6 +854,13 @@ records through **`manageCollection`**, not raw file I/O:
854
854
  / `fields` on large collections to keep the result small — e.g.
855
855
  `fields: ["id"]` to check for an id collision before an add.
856
856
  - **Delete** — remove the record file (`manageCollection` has no delete).
857
+ - **Aggregate — `queryItems`.** Counts, sums, averages, group-bys on ANY
858
+ collection via a structured query (`groupBy` / `aggregates` / `where` /
859
+ `orderBy` / `limit` — full shape in the "External data (CSV) collections"
860
+ section below). On file-backed collections it aggregates the ENRICHED
861
+ records, so computed fields (`derived` / `rollup` / `toggle`) are
862
+ queryable columns — "sum of invoice totals" works even when `total` is a
863
+ formula. Prefer it over doing arithmetic on `getItems` output.
857
864
  - **Cross-collection questions — `getOntology`.** Returns every collection in
858
865
  the workspace with its `primaryKey`, effective `displayField`, record count,
859
866
  and its `ref` / `embed` / `backlinks` / `rollup` relations (field → related slug, including refs
@@ -961,10 +968,11 @@ Semantics to remember (and to tell the user):
961
968
  logged). Fine for browsing — but NEVER compute an aggregate from
962
969
  `getItems` output on a large file (a capped scan gives a silently wrong
963
970
  number). Use `queryItems` instead.
964
- - **Aggregation — `manageCollection` `queryItems`** (dataSource collections
965
- only): a structured query over the WHOLE file (uncapped scan, DuckDB
966
- underneath). Answer counts / sums / averages / group-bys with it —
967
- don't shell out to python/pandas for questions it covers. Shape:
971
+ - **Aggregation — `manageCollection` `queryItems`**: a structured query over
972
+ the WHOLE file (uncapped scan, DuckDB underneath). Answer counts / sums /
973
+ averages / group-bys with it — don't shell out to python/pandas for
974
+ questions it covers. (It works on EVERY collection see the Records
975
+ section; this bullet is about the dataSource specifics.) Shape:
968
976
 
969
977
  ```json
970
978
  {
@@ -101,12 +101,14 @@ toggles, embeds) — the same numbers the user sees elsewhere.
101
101
  / shows one record at a time. Combinable with `fields`.
102
102
  - The primary key is always returned regardless of `fields`.
103
103
 
104
- ### Aggregation queries (dataSource/CSV collections; `read` capability)
104
+ ### Aggregation queries (`read` capability)
105
105
 
106
- On a collection backed by an external CSV (`dataSource` in its schema), a
107
- view can run structured aggregations over the WHOLE file the honest way
108
- to chart big data (the record read above is row-capped at 5,000; an
109
- aggregate computed from it would be silently wrong):
106
+ A view can run structured aggregations over the collection on a
107
+ `dataSource` (CSV) collection this scans the WHOLE file (the record read
108
+ above is row-capped at 5,000 there, so an aggregate computed from it would
109
+ be silently wrong); on a file-backed collection it aggregates the ENRICHED
110
+ records, so computed fields (`derived` / `rollup` / `toggle`) are queryable
111
+ columns:
110
112
 
111
113
  ```js
112
114
  const res = await fetch(dataUrl + "/query", {
@@ -130,10 +132,12 @@ const { rows } = await res.json(); // [{ Category, total, n }, ...] — chart th
130
132
  `groupBy`/`aggregates`; `orderBy` sorts by a groupBy column or an
131
133
  aggregate alias; result rows clamp at 1,000 by default.
132
134
  - Structured JSON only — there is **no SQL surface**, by design.
133
- - A file-backed (non-dataSource) collection answers 400 ("file-backed") —
134
- compute from the record read instead there.
135
- - Combine with `onChange` (below) to re-run the query when the CSV is
136
- replaced that's a live dashboard.
135
+ - Combine with `onChange` (below) to stay live: in the callback, re-run
136
+ **this POST query** (wrap it in your own `refresh()` and register that)
137
+ NOT the `?fields=` record read from the section above, which would
138
+ silently repaint an aggregation view with non-aggregated data. A
139
+ replaced CSV or edited records then redraw the chart — that's a live
140
+ dashboard.
137
141
 
138
142
  ### Writing records (only with the `write` capability)
139
143
 
@@ -43,9 +43,11 @@ unless the user already told you:
43
43
 
44
44
  - **Clone an existing Zenn repo** (they already write Zenn on GitHub). Get the
45
45
  repo URL, then:
46
+
46
47
  ```bash
47
48
  git clone <url> github/zenn
48
49
  ```
50
+
49
51
  `npx zenn` fetches zenn-cli on demand, so a missing local dependency is fine.
50
52
 
51
53
  - **Create a fresh project** (standard zenn-cli flow):
@@ -90,6 +92,7 @@ fill it in. **A published slug becomes the article URL and can't change** — pi
90
92
  it deliberately.
91
93
 
92
94
  **Step 3 — write the frontmatter** (Zenn house style):
95
+
93
96
  ```yaml
94
97
  ---
95
98
  title: "<a clear title, in the user's language>"
@@ -99,6 +102,7 @@ topics: ["MulmoClaude", "<related>"]
99
102
  published: true
100
103
  ---
101
104
  ```
105
+
102
106
  - **Always include `MulmoClaude` in `topics`.** At most 5 topics, no spaces
103
107
  inside a single topic.
104
108
  - `type` defaults to `tech`; `published` defaults to `true` (use `false` when
@@ -109,6 +113,33 @@ implementation (fenced ` ```lang ` blocks, runnable commands) → result → a s
109
113
  wrap-up. Informative but casual; describe the work, not yourself. Put images in
110
114
  `github/zenn/images/` and reference them as `![alt](/images/<file>)`.
111
115
 
116
+ Write it the way a person would, not the way a model defaults to. Each rule
117
+ below comes from real reader feedback — ignore them and the draft reads as
118
+ machine output:
119
+
120
+ - **Sound human, not like a generated listicle.** Prefer flowing prose over
121
+ walls of bullets, use bold sparingly, and let paragraphs connect into an
122
+ argument. If every section is just a heading plus a list, vary the rhythm.
123
+ - **No uncommon loanwords or jargon.** Use only terms a typical reader already
124
+ knows. Niche English acronyms, trendy katakana, and insider slang either get
125
+ dropped or replaced with plain words. A term the field itself hasn't settled
126
+ on ("code smell" and the like) is a red flag — say the plain thing instead
127
+ ("code that works now but bites you later").
128
+ - **Define every term and acronym on first use**, in plain language, and put the
129
+ foundational ones up front — define "DRY" before the article leans on it. A
130
+ reader should never hit a word they can't parse.
131
+ - **Carry a source's essence, don't just cite it.** When an idea comes from a
132
+ book or article, explain the idea itself in your own words; "see <book>"
133
+ teaches nothing. Search for the actual content when you're unsure what it says,
134
+ then write the substance.
135
+ - **Make the article self-contained.** Anything project-specific — a repo's
136
+ internals, a config choice, a domain constraint — needs enough general
137
+ background that a reader with zero context can follow. Running longer is fine
138
+ when the extra length buys understanding.
139
+ - **Match the stated audience.** "Explain to intermediate developers" means
140
+ patient explanations, concrete examples, real code from the work, and links —
141
+ not a terse summary.
142
+
112
143
  **Step 5 — save + preview.** Write `github/zenn/articles/<slug>.md`. Tell the
113
144
  user the path and how to preview: `cd github/zenn && npx zenn preview`
114
145
  (http://localhost:8000). Surface the title, slug, and topics.
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../../rolldown-runtime-D6vf50IK.cjs");
3
- const require_server = require("../../../server-Q7ld-FlZ.cjs");
3
+ const require_server = require("../../../server-Jaogu3Ky.cjs");
4
4
  const require_collection_paths = require("../../paths.cjs");
5
5
  const require_skill_bridge_index = require("../../../skill-bridge/index.cjs");
6
6
  const require_types = require("../../../types-CNqkLT4M.cjs");
@@ -1,4 +1,4 @@
1
- import { D as CollectionSchemaZ, S as acceptParsedSchema, _ as errorMessage, g as ONE_SECOND_MS, ht as collectionsRegistriesConfigPath, ot as writeFileAtomic, pt as safeRecordId, vt as log, w as loadCollection } from "../../../server--FgDORd3.js";
1
+ import { D as CollectionSchemaZ, S as acceptParsedSchema, _ as errorMessage, _t as collectionsRegistriesConfigPath, bt as log, ct as writeFileAtomic, g as ONE_SECOND_MS, ht as safeRecordId, w as loadCollection } from "../../../server-D2ibbixF.js";
2
2
  import { isSafeActionTemplatePath } from "../../paths.js";
3
3
  import { claudeSkillDir, dataSkillDir, mirrorSkillWrite } from "../../../skill-bridge/index.js";
4
4
  import { n as parseRegistryIndex, r as isRecord, t as OFFICIAL_REGISTRY_NAME } from "../../../types-BKVZrtyc.js";
@@ -8,12 +8,20 @@ export declare function quoteLiteral(value: string): string;
8
8
  * DuckDB's sniffer turns `001` into BIGINT 1, so leading zeros vanish
9
9
  * and distinct keys collapse. */
10
10
  export declare function readCsvArgs(primaryKey: string): string;
11
- /** Compile a validated query. Returns the SQL (whose FIRST placeholder is
12
- * the CSV path — the executor binds it) and the where-value parameters
13
- * that follow it. Callers MUST have run `CollectionQueryZ` first; this
14
- * function trusts the shape (aliases already charset-checked, orderBy
15
- * membership already enforced). */
11
+ /** Compile against a CSV file (the dataSource store's engine). */
16
12
  export declare function compileCsvQuery(query: CollectionQuery, primaryKey: string): {
17
13
  sql: string;
18
14
  params: unknown[];
19
15
  };
16
+ /** Compile against a JSONL file of ENRICHED records — the file-backed
17
+ * collections' engine (see `jsonlQuery.ts`). No VARCHAR key pin needed:
18
+ * enriched record ids are already strings. `sample_size=-1` makes the
19
+ * schema inference scan EVERY line — with the default sample, a sparse
20
+ * optional/derived field first appearing past the sample would not be
21
+ * inferred as a column and the query would binder-error on it (Codex P2
22
+ * on #2165). The full scan costs nothing extra here: aggregation reads
23
+ * the whole file anyway. */
24
+ export declare function compileJsonlQuery(query: CollectionQuery): {
25
+ sql: string;
26
+ params: unknown[];
27
+ };
@@ -30,6 +30,8 @@ export declare function dedupeByRecordId(items: CollectionItem[], primaryKey: st
30
30
  items: CollectionItem[];
31
31
  duplicates: number;
32
32
  };
33
+ export declare function cacheDir(): string;
34
+ export declare function queryCsv(sql: string, params: unknown[]): Promise<Record<string, unknown>[]>;
33
35
  /** Every row of the CSV as records — capped, deduped, id-encoded. The
34
36
  * key column is pinned to VARCHAR (see `readCsvArgs`). `workspaceRoot`
35
37
  * drives the per-read containment check; omitted, the configured host
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_promptSafety = require("../../promptSafety-DRd15gQ6.cjs");
3
- const require_server = require("../../server-Q7ld-FlZ.cjs");
3
+ const require_server = require("../../server-Jaogu3Ky.cjs");
4
4
  const require_collection_paths = require("../paths.cjs");
5
5
  exports.COMPUTED_TYPES = require_promptSafety.COMPUTED_TYPES;
6
6
  exports.CollectionQueryZ = require_server.CollectionQueryZ;
@@ -19,6 +19,7 @@ exports.buildCollectionActionSeedPrompt = require_server.buildCollectionActionSe
19
19
  exports.buildWorkspaceOntology = require_server.buildWorkspaceOntology;
20
20
  exports.collectionWritable = require_server.collectionWritable;
21
21
  exports.compileCsvQuery = require_server.compileCsvQuery;
22
+ exports.compileJsonlQuery = require_server.compileJsonlQuery;
22
23
  exports.compileRecordZ = require_server.compileRecordZ;
23
24
  exports.computeCollectionIcon = require_server.computeCollectionIcon;
24
25
  exports.computeSuccessor = require_server.computeSuccessor;
@@ -66,6 +67,7 @@ exports.resolveCreateItemId = require_server.resolveCreateItemId;
66
67
  exports.resolveDataDir = require_server.resolveDataDir;
67
68
  exports.resolveEvery = require_server.resolveEvery;
68
69
  exports.resolveTemplatePath = require_server.resolveTemplatePath;
70
+ exports.runQueryOverRows = require_server.runQueryOverRows;
69
71
  exports.safeRecordId = require_server.safeRecordId;
70
72
  exports.safeSlugName = require_server.safeSlugName;
71
73
  exports.schemaRelations = require_server.schemaRelations;
@@ -5,7 +5,8 @@ export * from './templatePath';
5
5
  export * from './io';
6
6
  export * from './store';
7
7
  export { MAX_CSV_ROWS, encodeCsvRecordId, decodeCsvRecordId, normalizeCsvValue, csvRowToItem, dedupeByRecordId } from './csvStore';
8
- export { compileCsvQuery } from './csvQuery';
8
+ export { compileCsvQuery, compileJsonlQuery } from './csvQuery';
9
+ export { runQueryOverRows } from './jsonlQuery';
9
10
  export { CollectionQueryZ, MAX_QUERY_ROWS, DEFAULT_QUERY_ROWS } from '../core/queryZ';
10
11
  export type { CollectionQuery, CollectionQueryAggregate, CollectionQueryOrder, CollectionQueryWhere } from '../core/queryZ';
11
12
  export * from './validate';
@@ -1,4 +1,4 @@
1
1
  import { I as COMPUTED_TYPES } from "../../promptSafety-rDWA9_JS.js";
2
- import { $ as promptPathsFor, A as validateCollectionRecords, B as dedupeByRecordId, C as discoverCollections, D as CollectionSchemaZ, E as toSummary, F as readOnlyRefusal, G as DEFAULT_QUERY_ROWS, H as normalizeCsvValue, I as storeFor, J as buildCollectionActionSeedPrompt, K as MAX_QUERY_ROWS, L as MAX_CSV_ROWS, M as compileRecordZ, N as recordFieldProblem, O as applyMutateAction, P as collectionWritable, Q as listItems, R as csvRowToItem, S as acceptParsedSchema, T as toDetail, U as compileCsvQuery, V as encodeCsvRecordId, W as CollectionQueryZ, X as generateItemId, Y as deleteItem, Z as isRegularFile, _t as getWorkspaceRoot, a as deleteCollection, at as writeItem, b as buildWorkspaceOntology, bt as setCollectionChangePublisher, c as computeSuccessor, ct as isContainedInRoot, d as isTriggerDue, dt as resolveDataDir, et as readCustomViewHtml, f as maybeSpawnSuccessor, ft as resolveTemplatePath, gt as configureCollectionHost, h as successorId, i as deleteCustomView, it as resolveCreateItemId, j as validateRecordObject, k as firstMutateParamProblem, l as daysInMonth, lt as isContainedInWorkspace, m as resolveEvery, mt as safeSlugName, n as MAX_UNSELECTIVE_ITEMS, nt as readItem, o as deleteCollectionRefusalMessage, p as parseCivil, pt as safeRecordId, q as buildActionSeedPrompt, r as makeManageCollectionTool, rt as readSkillTemplate, s as advanceTriggerDate, st as SCHEMA_FILE, t as MAX_SCHEMA_ISSUES, tt as readCustomViewI18n, u as formatCivil, ut as itemFilePath, v as computeCollectionIcon, vt as log, w as loadCollection, x as schemaRelations, y as enrichItems, yt as publishCollectionChange, z as decodeCsvRecordId } from "../../server--FgDORd3.js";
2
+ import { $ as isRegularFile, A as validateCollectionRecords, B as decodeCsvRecordId, C as discoverCollections, D as CollectionSchemaZ, E as toSummary, F as collectionWritable, G as compileJsonlQuery, H as encodeCsvRecordId, I as readOnlyRefusal, J as MAX_QUERY_ROWS, K as CollectionQueryZ, L as storeFor, M as compileRecordZ, N as recordFieldProblem, O as applyMutateAction, P as runQueryOverRows, Q as generateItemId, R as MAX_CSV_ROWS, S as acceptParsedSchema, St as setCollectionChangePublisher, T as toDetail, U as normalizeCsvValue, V as dedupeByRecordId, W as compileCsvQuery, X as buildCollectionActionSeedPrompt, Y as buildActionSeedPrompt, Z as deleteItem, a as deleteCollection, at as readSkillTemplate, b as buildWorkspaceOntology, bt as log, c as computeSuccessor, d as isTriggerDue, dt as isContainedInWorkspace, et as listItems, f as maybeSpawnSuccessor, ft as itemFilePath, gt as safeSlugName, h as successorId, ht as safeRecordId, i as deleteCustomView, it as readItem, j as validateRecordObject, k as firstMutateParamProblem, l as daysInMonth, lt as SCHEMA_FILE, m as resolveEvery, mt as resolveTemplatePath, n as MAX_UNSELECTIVE_ITEMS, nt as readCustomViewHtml, o as deleteCollectionRefusalMessage, ot as resolveCreateItemId, p as parseCivil, pt as resolveDataDir, q as DEFAULT_QUERY_ROWS, r as makeManageCollectionTool, rt as readCustomViewI18n, s as advanceTriggerDate, st as writeItem, t as MAX_SCHEMA_ISSUES, tt as promptPathsFor, u as formatCivil, ut as isContainedInRoot, v as computeCollectionIcon, vt as configureCollectionHost, w as loadCollection, x as schemaRelations, xt as publishCollectionChange, y as enrichItems, yt as getWorkspaceRoot, z as csvRowToItem } from "../../server-D2ibbixF.js";
3
3
  import { isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath } from "../paths.js";
4
- export { COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, collectionWritable, compileCsvQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
4
+ export { COMPUTED_TYPES, CollectionQueryZ, CollectionSchemaZ, DEFAULT_QUERY_ROWS, MAX_CSV_ROWS, MAX_QUERY_ROWS, MAX_SCHEMA_ISSUES, MAX_UNSELECTIVE_ITEMS, SCHEMA_FILE, acceptParsedSchema, advanceTriggerDate, applyMutateAction, buildActionSeedPrompt, buildCollectionActionSeedPrompt, buildWorkspaceOntology, collectionWritable, compileCsvQuery, compileJsonlQuery, compileRecordZ, computeCollectionIcon, computeSuccessor, configureCollectionHost, csvRowToItem, daysInMonth, decodeCsvRecordId, dedupeByRecordId, deleteCollection, deleteCollectionRefusalMessage, deleteCustomView, deleteItem, discoverCollections, encodeCsvRecordId, enrichItems, firstMutateParamProblem, formatCivil, generateItemId, getWorkspaceRoot, isContainedInRoot, isContainedInWorkspace, isRegularFile, isSafeActionTemplatePath, isSafeCustomViewI18nPath, isSafeCustomViewPath, isSafeTemplatePath, isTriggerDue, itemFilePath, listItems, loadCollection, log, makeManageCollectionTool, maybeSpawnSuccessor, normalizeCsvValue, parseCivil, promptPathsFor, publishCollectionChange, readCustomViewHtml, readCustomViewI18n, readItem, readOnlyRefusal, readSkillTemplate, recordFieldProblem, resolveCreateItemId, resolveDataDir, resolveEvery, resolveTemplatePath, runQueryOverRows, safeRecordId, safeSlugName, schemaRelations, setCollectionChangePublisher, storeFor, successorId, toDetail, toSummary, validateCollectionRecords, validateRecordObject, writeItem };
@@ -0,0 +1,4 @@
1
+ import { CollectionItem } from '../core/schema';
2
+ import { CollectionQuery } from '../core/queryZ';
3
+ /** Run a validated query over enriched collection rows. */
4
+ export declare function runQueryOverRows(rows: CollectionItem[], query: CollectionQuery): Promise<Record<string, unknown>[]>;
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
3
3
  const require_promptSafety = require("../promptSafety-DRd15gQ6.cjs");
4
4
  require("../collection/index.cjs");
5
- const require_server = require("../server-Q7ld-FlZ.cjs");
5
+ const require_server = require("../server-Jaogu3Ky.cjs");
6
6
  const require_notifier = require("../notifier-bS8IEeLA.cjs");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
@@ -1,6 +1,6 @@
1
1
  import { A as whenMatches } from "../promptSafety-rDWA9_JS.js";
2
2
  import "../collection/index.js";
3
- import { C as discoverCollections, Q as listItems, d as isTriggerDue, f as maybeSpawnSuccessor, nt as readItem, w as loadCollection, yt as publishCollectionChange } from "../server--FgDORd3.js";
3
+ import { C as discoverCollections, d as isTriggerDue, et as listItems, f as maybeSpawnSuccessor, it as readItem, w as loadCollection, xt as publishCollectionChange } from "../server-D2ibbixF.js";
4
4
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
5
5
  import path from "node:path";
6
6
  import { watch } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
3
3
  const require_promptSafety = require("../../promptSafety-DRd15gQ6.cjs");
4
- const require_server = require("../../server-Q7ld-FlZ.cjs");
4
+ const require_server = require("../../server-Jaogu3Ky.cjs");
5
5
  const require_ingestTypes = require("../../ingestTypes-Cev9q77C.cjs");
6
6
  const require_feeds_paths = require("../paths.cjs");
7
7
  const require_notifier = require("../../notifier-bS8IEeLA.cjs");
@@ -1,5 +1,5 @@
1
1
  import { L as FEED_SCHEDULES, R as INGEST_KINDS } from "../../promptSafety-rDWA9_JS.js";
2
- import { $ as promptPathsFor, C as discoverCollections, J as buildCollectionActionSeedPrompt, Q as listItems, Y as deleteItem, at as writeItem, dt as resolveDataDir, mt as safeSlugName, rt as readSkillTemplate } from "../../server--FgDORd3.js";
2
+ import { C as discoverCollections, X as buildCollectionActionSeedPrompt, Z as deleteItem, at as readSkillTemplate, et as listItems, gt as safeSlugName, pt as resolveDataDir, st as writeItem, tt as promptPathsFor } from "../../server-D2ibbixF.js";
3
3
  import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-B-dXxUF9.js";
4
4
  import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
5
5
  import { d as publish, n as clear } from "../../notifier-ChpY0XrY.js";
@@ -1,6 +1,7 @@
1
1
  import { default as http } from 'node:http';
2
2
  import { Credentials } from 'google-auth-library';
3
3
  import { fetchWithTimeout } from './fetch.js';
4
+ import { IssuedVia } from './tokenStore.js';
4
5
  export declare const GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.events";
5
6
  /** Minimal read scope for the calendar list + per-calendar colours.
6
7
  * `calendar.events` already covers reading/writing events on any calendar, but
@@ -19,6 +20,9 @@ export interface AuthorizeGoogleOptions {
19
20
  /** Called with the consent URL; open it in a browser (and/or print it). */
20
21
  onAuthUrl?: (url: string) => void;
21
22
  timeoutMs?: number;
23
+ /** Abort a still-pending consent (a settings-UI restart, an explicit cancel).
24
+ * Rejects the flow with {@link GOOGLE_AUTH_CANCELLED} and closes the loopback. */
25
+ signal?: AbortSignal;
22
26
  }
23
27
  export declare function getGoogleAccessToken(home?: string): Promise<string>;
24
28
  /** The revoke POST, injectable for tests. */
@@ -28,5 +32,10 @@ export type RevokeFetch = typeof fetchWithTimeout;
28
32
  * already consider the token invalid, and keeping the file would leave the
29
33
  * user unable to unlink. */
30
34
  export declare function unlinkGoogle(home?: string, revokeFetch?: RevokeFetch): Promise<void>;
31
- export declare const waitForAuthCode: (server: http.Server, expectedState: string, timeoutMs: number) => Promise<string>;
35
+ /** Reject reason when a pending flow is torn down (a settings-UI restart or an
36
+ * explicit cancel), kept distinct from a real authorization error so callers
37
+ * can treat it as "user changed their mind", not "linking failed". */
38
+ export declare const GOOGLE_AUTH_CANCELLED = "authorization cancelled";
39
+ export declare const waitForAuthCode: (server: http.Server, expectedState: string, timeoutMs: number, signal?: AbortSignal) => Promise<string>;
40
+ export declare const commitLinkedTokens: (tokens: Credentials, issuedVia: IssuedVia, opts: AuthorizeGoogleOptions) => Promise<Credentials>;
32
41
  export declare function authorizeGoogle(opts?: AuthorizeGoogleOptions): Promise<Credentials>;
@@ -7,6 +7,7 @@ export interface GoogleAuthFlow {
7
7
  start: () => Promise<{
8
8
  authUrl: string;
9
9
  }>;
10
+ cancel: () => void;
10
11
  status: () => GoogleAuthFlowStatus;
11
12
  }
12
13
  export declare const createGoogleAuthFlow: (authorize: typeof authorizeGoogle) => GoogleAuthFlow;
@@ -453,29 +453,50 @@ var respondHtml = (res, status, message) => {
453
453
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
454
454
  res.end(`<html><body><h3>${message}</h3></body></html>`);
455
455
  };
456
- var waitForAuthCode = (server, expectedState, timeoutMs) => new Promise((resolve, reject) => {
456
+ /** Reject reason when a pending flow is torn down (a settings-UI restart or an
457
+ * explicit cancel), kept distinct from a real authorization error so callers
458
+ * can treat it as "user changed their mind", not "linking failed". */
459
+ var GOOGLE_AUTH_CANCELLED = "authorization cancelled";
460
+ var handleCallbackRequest = (req, res, expectedState, settle) => {
461
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
462
+ if (url.pathname !== CALLBACK_PATH) {
463
+ res.writeHead(404);
464
+ res.end();
465
+ return;
466
+ }
467
+ if (url.searchParams.get("state") !== expectedState) {
468
+ respondHtml(res, 400, "Invalid authorization callback. You can close this tab.");
469
+ return;
470
+ }
471
+ try {
472
+ const code = authCodeFromCallback(url, expectedState);
473
+ respondHtml(res, 200, "Authorization complete — you can close this tab.");
474
+ settle.resolve(code);
475
+ } catch (err) {
476
+ respondHtml(res, 400, "Authorization failed — see the terminal for details. You can close this tab.");
477
+ settle.reject(err);
478
+ }
479
+ };
480
+ var waitForAuthCode = (server, expectedState, timeoutMs, signal) => new Promise((resolve, reject) => {
457
481
  const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);
458
- server.on("request", (req, res) => {
459
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
460
- if (url.pathname !== CALLBACK_PATH) {
461
- res.writeHead(404);
462
- res.end();
463
- return;
482
+ const settle = {
483
+ resolve: (code) => {
484
+ clearTimeout(timer);
485
+ resolve(code);
486
+ },
487
+ reject: (err) => {
488
+ clearTimeout(timer);
489
+ reject(err instanceof Error ? err : new Error(String(err)));
464
490
  }
465
- if (url.searchParams.get("state") !== expectedState) {
466
- respondHtml(res, 400, "Invalid authorization callback. You can close this tab.");
491
+ };
492
+ if (signal) {
493
+ if (signal.aborted) {
494
+ settle.reject(/* @__PURE__ */ new Error(GOOGLE_AUTH_CANCELLED));
467
495
  return;
468
496
  }
469
- clearTimeout(timer);
470
- try {
471
- const code = authCodeFromCallback(url, expectedState);
472
- respondHtml(res, 200, "Authorization complete — you can close this tab.");
473
- resolve(code);
474
- } catch (err) {
475
- respondHtml(res, 400, "Authorization failed — see the terminal for details. You can close this tab.");
476
- reject(err);
477
- }
478
- });
497
+ signal.addEventListener("abort", () => settle.reject(/* @__PURE__ */ new Error(GOOGLE_AUTH_CANCELLED)), { once: true });
498
+ }
499
+ server.on("request", (req, res) => handleCallbackRequest(req, res, expectedState, settle));
479
500
  });
480
501
  var buildConsentUrl = (client, codeChallenge, state) => client.generateAuthUrl({
481
502
  access_type: "offline",
@@ -499,7 +520,7 @@ var authorizeWithLocalClient = async (secret, server, port, opts) => {
499
520
  if (!codeChallenge) throw new Error("failed to derive a PKCE code challenge");
500
521
  const state = (0, node_crypto.randomBytes)(STATE_BYTES).toString("hex");
501
522
  opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));
502
- const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);
523
+ const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS, opts.signal);
503
524
  const { tokens } = await client.getToken({
504
525
  code,
505
526
  codeVerifier
@@ -512,11 +533,18 @@ var authorizeWithBroker = async (server, port, opts) => {
512
533
  const { authUrl, state } = await brokerStart(port, codeChallenge);
513
534
  opts.onAuthUrl?.(authUrl);
514
535
  return await brokerExchange({
515
- code: await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS),
536
+ code: await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS, opts.signal),
516
537
  state,
517
538
  codeVerifier
518
539
  });
519
540
  };
541
+ var commitLinkedTokens = async (tokens, issuedVia, opts) => {
542
+ if (opts.signal?.aborted) throw new Error(GOOGLE_AUTH_CANCELLED);
543
+ return await saveGoogleTokens({
544
+ ...tokens,
545
+ issuedVia
546
+ }, opts.home);
547
+ };
520
548
  async function authorizeGoogle(opts = {}) {
521
549
  const presence = await clientSecretPresence(opts.home);
522
550
  if (presence === "ambiguous") await loadClientSecret(opts.home);
@@ -524,10 +552,7 @@ async function authorizeGoogle(opts = {}) {
524
552
  const { server, port } = await startLoopbackServer();
525
553
  try {
526
554
  const issuedVia = useLocalClient ? "local" : "broker";
527
- return await saveGoogleTokens({
528
- ...useLocalClient ? await authorizeWithLocalClient(await loadClientSecret(opts.home), server, port, opts) : await authorizeWithBroker(server, port, opts),
529
- issuedVia
530
- }, opts.home);
555
+ return await commitLinkedTokens(useLocalClient ? await authorizeWithLocalClient(await loadClientSecret(opts.home), server, port, opts) : await authorizeWithBroker(server, port, opts), issuedVia, opts);
531
556
  } finally {
532
557
  server.close();
533
558
  }
@@ -535,25 +560,38 @@ async function authorizeGoogle(opts = {}) {
535
560
  //#endregion
536
561
  //#region src/google/authFlow.ts
537
562
  var createGoogleAuthFlow = (authorize) => {
538
- let inFlightStart = null;
539
563
  let flowRunning = false;
540
564
  let lastError = null;
541
- const launchFlow = () => new Promise((resolve, reject) => {
565
+ let active = null;
566
+ const launch = () => {
567
+ const controller = new AbortController();
568
+ active = controller;
542
569
  flowRunning = true;
543
- authorize({ onAuthUrl: (url) => resolve({ authUrl: url }) }).then(() => log.info("google", "authorize flow completed")).catch((err) => {
544
- lastError = errorMessage(err);
545
- log.warn("google", "authorize flow failed", { error: lastError });
546
- reject(err instanceof Error ? err : new Error(String(err)));
547
- }).finally(() => {
548
- flowRunning = false;
549
- inFlightStart = null;
570
+ return new Promise((resolve, reject) => {
571
+ authorize({
572
+ onAuthUrl: (url) => resolve({ authUrl: url }),
573
+ signal: controller.signal
574
+ }).then(() => log.info("google", "authorize flow completed")).catch((err) => {
575
+ if (!controller.signal.aborted) {
576
+ lastError = errorMessage(err);
577
+ log.warn("google", "authorize flow failed", { error: lastError });
578
+ }
579
+ reject(err instanceof Error ? err : new Error(String(err)));
580
+ }).finally(() => {
581
+ if (active === controller) {
582
+ flowRunning = false;
583
+ active = null;
584
+ }
585
+ });
550
586
  });
551
- });
587
+ };
588
+ const cancel = () => {
589
+ active?.abort();
590
+ };
552
591
  const start = () => {
553
- if (inFlightStart) return inFlightStart;
592
+ cancel();
554
593
  lastError = null;
555
- inFlightStart = launchFlow();
556
- return inFlightStart;
594
+ return launch();
557
595
  };
558
596
  const status = () => ({
559
597
  pending: flowRunning,
@@ -561,6 +599,7 @@ var createGoogleAuthFlow = (authorize) => {
561
599
  });
562
600
  return {
563
601
  start,
602
+ cancel,
564
603
  status
565
604
  };
566
605
  };
@@ -863,6 +902,7 @@ async function deleteDriveFile(accessToken, input) {
863
902
  }
864
903
  //#endregion
865
904
  exports.DEFAULT_LIST_MAX_RESULTS = DEFAULT_LIST_MAX_RESULTS;
905
+ exports.GOOGLE_AUTH_CANCELLED = GOOGLE_AUTH_CANCELLED;
866
906
  exports.GOOGLE_CALENDARLIST_SCOPE = GOOGLE_CALENDARLIST_SCOPE;
867
907
  exports.GOOGLE_CALENDAR_SCOPE = GOOGLE_CALENDAR_SCOPE;
868
908
  exports.GOOGLE_DRIVE_FILE_SCOPE = GOOGLE_DRIVE_FILE_SCOPE;
@@ -879,6 +919,7 @@ exports.buildMultipartBody = buildMultipartBody;
879
919
  exports.calendarApiError = calendarApiError;
880
920
  exports.clientSecretPresence = clientSecretPresence;
881
921
  exports.collectCalendarPages = collectCalendarPages;
922
+ exports.commitLinkedTokens = commitLinkedTokens;
882
923
  exports.completeTask = completeTask;
883
924
  exports.configureGoogleHost = configureGoogleHost;
884
925
  exports.createCalendarEvent = createCalendarEvent;