@quantod/qq 1.1.18 → 1.2.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/src/http.ts CHANGED
@@ -4,7 +4,8 @@ import { createServer } from 'node:net';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import {
6
6
  makePipeline, deletePipeline, push, claim, release,
7
- batchRead, status, unstick, loadFile, batchWrite, QQError,
7
+ query, status, unstick, uploadFile, uploadData, QQError,
8
+ type PayloadSelect, type ClaimStrategy,
8
9
  } from './commands.js';
9
10
 
10
11
  export interface HttpServer {
@@ -65,8 +66,8 @@ export async function startHttp(fixedPort?: number): Promise<HttpServer> {
65
66
  });
66
67
 
67
68
  app.post('/pipelines/:pipeline/push', (req, res) => {
68
- const { stage, id, payload, priority, payloadFormat } = req.body ?? {};
69
- wrap(res, () => ({ id: push(req.params.pipeline, stage, id, { payload, priority, payloadFormat }) }));
69
+ const { stage, id, payload, priority } = req.body ?? {};
70
+ wrap(res, () => ({ id: push(req.params.pipeline, stage, id, { payload, priority }) }));
70
71
  });
71
72
 
72
73
  app.post('/pipelines/:pipeline/claim', (req, res) => {
@@ -76,8 +77,8 @@ export async function startHttp(fixedPort?: number): Promise<HttpServer> {
76
77
 
77
78
  app.post('/pipelines/:pipeline/release/:seq', (req, res) => {
78
79
  const seq = parseInt(req.params.seq, 10);
79
- const { target, payload, replace, priority, payloadFormat } = req.body ?? {};
80
- wrap(res, () => { release(req.params.pipeline, seq, { target, payload, replace, priority, payloadFormat }); });
80
+ const { target, payload, replace, priority } = req.body ?? {};
81
+ wrap(res, () => { release(req.params.pipeline, seq, { target, payload, replace, priority }); });
81
82
  });
82
83
 
83
84
  app.get('/pipelines/:pipeline/status', (req, res) => {
@@ -85,38 +86,51 @@ export async function startHttp(fixedPort?: number): Promise<HttpServer> {
85
86
  });
86
87
 
87
88
  app.get('/pipelines/:pipeline/items', (req, res) => {
88
- const { stage = '*', includePayload, claimed, ids, createdAfter, createdBefore, modifiedAfter, limit, offset, strategy } = req.query;
89
- wrap(res, () => batchRead(
90
- req.params.pipeline,
91
- String(stage),
92
- {
93
- claimed: claimed !== undefined ? String(claimed).toLowerCase() === 'true' : undefined,
94
- ids: ids ? String(ids).split(',') : undefined,
95
- created_after: createdAfter ? String(createdAfter) : undefined,
96
- created_before: createdBefore ? String(createdBefore) : undefined,
97
- modified_after: modifiedAfter ? String(modifiedAfter) : undefined,
98
- limit: limit ? Number(limit) : undefined,
99
- offset: offset ? Number(offset) : undefined,
100
- },
101
- String(includePayload).toLowerCase() === 'true',
102
- strategy ? String(strategy) as import('./commands.js').ClaimStrategy : undefined,
103
- ));
89
+ const { stage = '*', filter: filterStr, select: selectStr, claimed, ids, createdAfter, createdBefore, modifiedAfter, limit, offset, strategy } = req.query;
90
+ wrap(res, () => {
91
+ let filter: Record<string, unknown> | undefined;
92
+ if (filterStr) {
93
+ try { filter = JSON.parse(String(filterStr)); }
94
+ catch { throw new QQError('filter must be valid JSON'); }
95
+ }
96
+ let select: PayloadSelect | undefined;
97
+ if (selectStr) {
98
+ const s = String(selectStr);
99
+ select = s === '*' ? '*' : s.split(',').map(f => f.trim()).filter(Boolean);
100
+ }
101
+ return query(
102
+ req.params.pipeline,
103
+ String(stage),
104
+ {
105
+ claimed: claimed !== undefined ? String(claimed).toLowerCase() === 'true' : undefined,
106
+ ids: ids ? String(ids).split(',') : undefined,
107
+ created_after: createdAfter ? String(createdAfter) : undefined,
108
+ created_before: createdBefore ? String(createdBefore) : undefined,
109
+ modified_after: modifiedAfter ? String(modifiedAfter) : undefined,
110
+ limit: limit ? Number(limit) : undefined,
111
+ offset: offset ? Number(offset) : undefined,
112
+ filter,
113
+ select,
114
+ },
115
+ strategy ? String(strategy) as ClaimStrategy : undefined,
116
+ );
117
+ });
104
118
  });
105
119
 
106
- app.post('/pipelines/:pipeline/load', (req, res) => {
120
+ app.post('/pipelines/:pipeline/upload-file', (req, res) => {
107
121
  const { path: filePath, stage, deleteAfter, duplicates } = req.body ?? {};
108
122
  wrap(res, () => {
109
- const results = loadFile(filePath, req.params.pipeline, { stage, deleteAfter, duplicates });
123
+ const results = uploadFile(filePath, req.params.pipeline, { stage, deleteAfter, duplicates });
110
124
  const counts = { new: 0, duplicate: 0, error: 0 };
111
125
  for (const r of results) counts[r.status]++;
112
126
  return { summary: counts, items: results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status })) };
113
127
  });
114
128
  });
115
129
 
116
- app.post('/pipelines/:pipeline/batch-write', (req, res) => {
130
+ app.post('/pipelines/:pipeline/upload-data', (req, res) => {
117
131
  const { data, format, stage, duplicates } = req.body ?? {};
118
132
  wrap(res, () => {
119
- const results = batchWrite(data, format, req.params.pipeline, { stage, duplicates });
133
+ const results = uploadData(data, format, req.params.pipeline, { stage, duplicates });
120
134
  const counts = { new: 0, duplicate: 0, error: 0 };
121
135
  for (const r of results) counts[r.status]++;
122
136
  return { summary: counts, items: results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status })) };
package/src/index.ts CHANGED
@@ -4,8 +4,8 @@ import { join } from 'node:path';
4
4
  import { dump } from 'js-yaml';
5
5
  import {
6
6
  makePipeline, listPipelines, deletePipeline, push, claim, release,
7
- batchRead, status, unstick, loadFile, batchWrite, backup, QQError,
8
- type FilterOptions, type PushOptions, type ClaimOptions, type ClaimStrategy, type PayloadFormat, type DuplicateMode, type BatchWriteFormat,
7
+ query, status, unstick, uploadFile, uploadData, backup, QQError,
8
+ type FilterOptions, type PushOptions, type ClaimOptions, type ClaimStrategy, type PayloadFormat, type DuplicateMode, type BatchWriteFormat, type PayloadSelect,
9
9
  } from './commands.js';
10
10
 
11
11
  function out(data: unknown): void {
@@ -112,7 +112,7 @@ program
112
112
  program
113
113
  .command('push <path> [id]')
114
114
  .option('--priority <n>', 'item priority (higher = claimed first)', parseFloatArg, 0.0)
115
- .option('--payload-format <yaml|json|text>', 'payload validation: yaml (default), json (minified), text (none)')
115
+ .option('--payload-format <yaml|json>', 'payload parsing format: yaml (default) or json')
116
116
  .action((path, id, opts) => {
117
117
  try {
118
118
  const { pipeline, stage = '' } = parsePath(path);
@@ -143,7 +143,7 @@ program
143
143
  .option('--target <stage>', 'move item to this stage (default: stay in current stage)')
144
144
  .option('--replace')
145
145
  .option('--priority <n>', 'update item priority', parseFloatArg)
146
- .option('--payload-format <yaml|json|text>', 'payload validation: yaml (default), json (minified), text (none)')
146
+ .option('--payload-format <yaml|json>', 'payload parsing format: yaml (default) or json')
147
147
  .action((pipeline, seqStr, opts) => {
148
148
  try {
149
149
  const seq = parseInt(seqStr, 10);
@@ -154,15 +154,24 @@ program
154
154
  } catch (e) { e instanceof QQError ? fail(e.message) : fail(String(e)); }
155
155
  });
156
156
 
157
- addFilterOptions(program.command('batch-read <path>'))
158
- .option('--include-payload', 'include payload in output')
157
+ addFilterOptions(program.command('query <path>'))
158
+ .option('--filter <json>', 'MongoDB-style payload filter as JSON')
159
+ .option('--select <fields>', 'payload fields: * for all, or comma-separated paths (e.g. status,meta.author)')
159
160
  .option('--strategy <priority|fifo|lifo|random>', 'sort order (default: fifo)', 'fifo')
160
161
  .action((path, opts) => {
161
162
  try {
162
163
  const { pipeline, stage = '*' } = parsePath(path);
163
164
  const filters = collectFilters(opts);
164
165
  if (filters.claimed === undefined) filters.claimed = false;
165
- out(batchRead(pipeline, stage, filters, opts.includePayload ?? false, opts.strategy as ClaimStrategy));
166
+ if (opts.filter) {
167
+ try { filters.filter = JSON.parse(opts.filter as string); }
168
+ catch { fail('--filter must be valid JSON'); }
169
+ }
170
+ if (opts.select) {
171
+ const s = opts.select as string;
172
+ filters.select = s === '*' ? '*' : s.split(',').map((f: string) => f.trim()).filter(Boolean) as PayloadSelect;
173
+ }
174
+ out(query(pipeline, stage, filters, opts.strategy as ClaimStrategy));
166
175
  }
167
176
  catch (e) { fail(e instanceof Error ? e.message : String(e)); }
168
177
  });
@@ -186,14 +195,14 @@ addReapFilterOptions(program.command('unstick <path>'))
186
195
  });
187
196
 
188
197
  program
189
- .command('load-file <path> <pipeline>')
198
+ .command('upload-file <path> <pipeline>')
190
199
  .description('Load multiple items from a file into a pipeline (JSONL, JSON array, YAML array, CSV)')
191
200
  .option('--stage <stage>', 'default stage for records that do not specify one')
192
201
  .option('--delete-after', 'delete the file after loading')
193
202
  .option('--duplicates <ignore|append|replace>', 'how to handle duplicate ids: ignore (default), append payload, or replace payload', 'ignore')
194
203
  .action((filePath, pipeline, opts) => {
195
204
  try {
196
- const results = loadFile(filePath, pipeline, { stage: opts.stage, deleteAfter: opts.deleteAfter ?? false, duplicates: opts.duplicates as DuplicateMode });
205
+ const results = uploadFile(filePath, pipeline, { stage: opts.stage, deleteAfter: opts.deleteAfter ?? false, duplicates: opts.duplicates as DuplicateMode });
197
206
  const counts = { new: 0, duplicate: 0, error: 0 };
198
207
  for (const r of results) counts[r.status]++;
199
208
  const items = results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status }));
@@ -202,7 +211,7 @@ program
202
211
  });
203
212
 
204
213
  program
205
- .command('batch-write <pipeline>')
214
+ .command('upload-data <pipeline>')
206
215
  .description('Write multiple items from stdin into a pipeline (JSONL, JSON array, YAML array, CSV)')
207
216
  .requiredOption('--format <jsonl|json|yaml|csv>', 'data format')
208
217
  .option('--stage <stage>', 'default stage for records that do not specify one')
@@ -211,7 +220,7 @@ program
211
220
  try {
212
221
  const data = readStdin();
213
222
  if (!data) fail('no data on stdin');
214
- const results = batchWrite(data, opts.format as BatchWriteFormat, pipeline, { stage: opts.stage, duplicates: opts.duplicates as DuplicateMode });
223
+ const results = uploadData(data, opts.format as BatchWriteFormat, pipeline, { stage: opts.stage, duplicates: opts.duplicates as DuplicateMode });
215
224
  const counts = { new: 0, duplicate: 0, error: 0 };
216
225
  for (const r of results) counts[r.status]++;
217
226
  const items = results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status }));
package/src/mcp.ts CHANGED
@@ -7,7 +7,7 @@ import { z } from 'zod';
7
7
  import { dump } from 'js-yaml';
8
8
  import {
9
9
  makePipeline, deletePipeline, push, claim, release,
10
- batchRead, status, unstick, loadFile, batchWrite, readFile,
10
+ query, status, unstick, uploadFile, uploadData, readFile,
11
11
  } from './commands.js';
12
12
  import { startHttp } from './http.js';
13
13
  import { renderJavaScriptSdk } from './sdk-templates/javascript.js';
@@ -81,12 +81,11 @@ export async function startMcp(fixedPort?: number): Promise<void> {
81
81
  pipeline: z.string(),
82
82
  stage: z.string().optional(),
83
83
  id: z.string().optional(),
84
- payload: z.string().optional(),
84
+ payload: z.record(z.unknown()).optional(),
85
85
  priority: z.number().optional(),
86
- payloadFormat: z.enum(['yaml', 'json', 'text']).optional(),
87
86
  },
88
- }, ({ pipeline, stage, id, payload, priority, payloadFormat }) => {
89
- try { return ok(`id: ${push(pipeline, stage, id, { payload, priority, payloadFormat })}`); }
87
+ }, ({ pipeline, stage, id, payload, priority }) => {
88
+ try { return ok(`id: ${push(pipeline, stage, id, { payload, priority })}`); }
90
89
  catch (e) { return err(e); }
91
90
  });
92
91
 
@@ -109,13 +108,12 @@ export async function startMcp(fixedPort?: number): Promise<void> {
109
108
  pipeline: z.string(),
110
109
  seq: z.number(),
111
110
  target: z.string().optional(),
112
- payload: z.string().optional(),
111
+ payload: z.record(z.unknown()).optional(),
113
112
  replace: z.boolean().optional(),
114
113
  priority: z.number().optional(),
115
- payloadFormat: z.enum(['yaml', 'json', 'text']).optional(),
116
114
  },
117
- }, ({ pipeline, seq, target, payload, replace, priority, payloadFormat }) => {
118
- try { release(pipeline, seq, { target, payload, replace, priority, payloadFormat }); return ok('ok: true'); }
115
+ }, ({ pipeline, seq, target, payload, replace, priority }) => {
116
+ try { release(pipeline, seq, { target, payload, replace, priority }); return ok('ok: true'); }
119
117
  catch (e) { return err(e); }
120
118
  });
121
119
 
@@ -127,12 +125,13 @@ export async function startMcp(fixedPort?: number): Promise<void> {
127
125
  catch (e) { return err(e); }
128
126
  });
129
127
 
130
- server.registerTool('batch_read', {
131
- description: 'Read items from a pipeline stage without claiming. Read guide before using.',
128
+ server.registerTool('query', {
129
+ description: 'Query items from a pipeline stage without claiming. Read guide before using.',
132
130
  inputSchema: {
133
131
  pipeline: z.string(),
134
132
  stage: z.string().optional(),
135
- includePayload: z.boolean().optional(),
133
+ filter: z.record(z.unknown()).optional(),
134
+ select: z.union([z.literal('*'), z.array(z.string())]).optional(),
136
135
  claimed: z.boolean().optional(),
137
136
  ids: z.array(z.string()).optional(),
138
137
  createdAfter: z.union([z.number(), z.string()]).optional(),
@@ -142,12 +141,12 @@ export async function startMcp(fixedPort?: number): Promise<void> {
142
141
  offset: z.number().optional(),
143
142
  strategy: z.enum(['priority', 'fifo', 'lifo', 'random']).optional(),
144
143
  },
145
- }, ({ pipeline, stage, includePayload, claimed, ids, createdAfter, createdBefore, modifiedAfter, limit, offset, strategy }) => {
144
+ }, ({ pipeline, stage, filter, select, claimed, ids, createdAfter, createdBefore, modifiedAfter, limit, offset, strategy }) => {
146
145
  try {
147
- const items = batchRead(pipeline, stage ?? '*', {
146
+ const items = query(pipeline, stage ?? '*', {
148
147
  claimed, ids, created_after: createdAfter, created_before: createdBefore,
149
- modified_after: modifiedAfter, limit, offset,
150
- }, includePayload ?? false, strategy);
148
+ modified_after: modifiedAfter, limit, offset, filter, select,
149
+ }, strategy);
151
150
  return ok(dump(items, { lineWidth: -1 }));
152
151
  } catch (e) { return err(e); }
153
152
  });
@@ -191,8 +190,8 @@ export async function startMcp(fixedPort?: number): Promise<void> {
191
190
  return ok(snippet);
192
191
  });
193
192
 
194
- server.registerTool('load_file', {
195
- description: 'Load multiple items from a file into a pipeline. Supports JSONL, JSON array, YAML array, CSV. Read guide before using.',
193
+ server.registerTool('upload_file', {
194
+ description: 'Upload items from a file into a pipeline. Supports JSONL, JSON array, YAML array, CSV. Read guide before using.',
196
195
  inputSchema: {
197
196
  path: z.string(),
198
197
  pipeline: z.string(),
@@ -202,7 +201,7 @@ export async function startMcp(fixedPort?: number): Promise<void> {
202
201
  },
203
202
  }, ({ path: filePath, pipeline, stage, deleteAfter, duplicates }) => {
204
203
  try {
205
- const results = loadFile(filePath, pipeline, { stage, deleteAfter, duplicates });
204
+ const results = uploadFile(filePath, pipeline, { stage, deleteAfter, duplicates });
206
205
  const counts = { new: 0, duplicate: 0, error: 0 };
207
206
  for (const r of results) counts[r.status]++;
208
207
  const items = results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status }));
@@ -210,8 +209,8 @@ export async function startMcp(fixedPort?: number): Promise<void> {
210
209
  } catch (e) { return err(e); }
211
210
  });
212
211
 
213
- server.registerTool('batch_write', {
214
- description: 'Write multiple items from inline data into a pipeline. Supports JSONL, JSON array, YAML array, CSV. Read guide before using.',
212
+ server.registerTool('upload_data', {
213
+ description: 'Upload multiple items from inline data into a pipeline. Supports JSONL, JSON array, YAML array, CSV. Read guide before using.',
215
214
  inputSchema: {
216
215
  data: z.string(),
217
216
  format: z.enum(['jsonl', 'json', 'yaml', 'csv']),
@@ -221,7 +220,7 @@ export async function startMcp(fixedPort?: number): Promise<void> {
221
220
  },
222
221
  }, ({ data, format, pipeline, stage, duplicates }) => {
223
222
  try {
224
- const results = batchWrite(data, format, pipeline, { stage, duplicates });
223
+ const results = uploadData(data, format, pipeline, { stage, duplicates });
225
224
  const counts = { new: 0, duplicate: 0, error: 0 };
226
225
  for (const r of results) counts[r.status]++;
227
226
  const items = results.map(r => ({ [r.id]: r.message ? `${r.status}: ${r.message}` : r.status }));
@@ -6,7 +6,7 @@ Read the `guide` resource first — this document covers Chrome-specific pattern
6
6
 
7
7
  ## How to get data out of Chrome
8
8
 
9
- Chrome devtools and connectors usually struggle to get large data packages from web pages in Chrome. Solution: download + load_file. The browser's download API has no mixed content restrictions. Trigger a download from the HTTPS page, then call the `load_file` MCP tool to ingest the file. Don't route large data through the context.
9
+ Chrome devtools and connectors usually struggle to get large data packages from web pages in Chrome. Solution: download + upload_file. The browser's download API has no mixed content restrictions. Trigger a download from the HTTPS page, then call the `upload_file` MCP tool to ingest the file. Don't route large data through the context.
10
10
 
11
11
  **In the injected script (HTTPS page):**
12
12
  ```javascript
@@ -19,9 +19,9 @@ a.download = 'qq-data.json';
19
19
  a.click();
20
20
  ```
21
21
 
22
- **Then call load_file:**
22
+ **Then call upload_file:**
23
23
  ```
24
- load_file(
24
+ upload_file(
25
25
  path: "$HOME/Downloads/qq-data.json",
26
26
  pipeline: "my_pypeline",
27
27
  stage: "inbox",