@wirecat/cli-core 0.439.0 → 0.441.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/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.441.0 — 26.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`tasks list claimed_by:<holder>` lists the tasks a holder has claimed**, and `claimed_by:*` every claimed task.
15
+ A caller that wanted one agent's claims read every open task and filtered them itself. The filter matches live
16
+ claims only: a claim whose lease has lapsed holds nothing, as `tasks claim` and the list's own `claimed_by` field
17
+ already read it, so `NOT claimed_by:*` lists what no one is working on. A wildcard such as `claimed_by:claude-code:*`
18
+ matches by pattern.
19
+
20
+ ### Fixed
21
+
22
+ - **`tasks claim` and `tasks release` work on a project task**, which belongs to no messenger account, as `tasks show`
23
+ and `tasks update` already did. They refused it as "no task for this account", so memo, which mounts these
24
+ commands, could not claim its own project tasks.
25
+
26
+ ## 0.440.0 — 26.10.2026
27
+
28
+ ### Added
29
+
30
+ - **A CLI's own command can keep its command line in its run record.** A command the CLI records itself with
31
+ `recorded` or `startRecording` kept no `argv` and no agent, since only cli-core's own commands built that part of the
32
+ record. `runContextOf` and the `RunContext` type are now exported from `@wirecat/cli-core/cli`. To adopt, pass
33
+ `context: runContextOf({ bin, command, program: rootOf(command), argv: environmentOf(command).argv, env,
34
+ recordInput: settings.recordInput !== false })` to the call; a CLI with its own program passes its own root command
35
+ and the words it parsed. Left out, `recordInput` masks every value that may be content. A CLI that records through
36
+ `commandRuns` under `run()` gets the command line with no change; its `settings()` may return `recordInput`, and when
37
+ it does not, the typed words are kept with secrets hidden.
38
+ - **`secretInput` also marks a whole command**: every word typed after it that is not an option is `[redacted]` in a
39
+ run record. For a command that refuses its arguments, such as one that asks for a phone number instead of taking it,
40
+ this hides what was typed without declaring an argument, so its usage line and contract stay the same.
41
+
42
+ ### Changed — may break callers
43
+
44
+ - **`--record` help says what a run keeps now.** It read "ids and timings, never content", which stopped being true
45
+ in 0.439.0; it now reads "keep this run, with the command as typed and secrets hidden; record_input false leaves the
46
+ typed words out". Every CLI's help snapshot and `docs/commands.md` change with it. `recordRuns` no longer takes
47
+ `content`, which only that line used.
48
+
49
+ ### Security
50
+
51
+ - **A phone number no longer reaches a run record.** Since 0.439.0 a run keeps the words as typed, so a number typed
52
+ after `contacts lookup`, which the command refuses, was kept in `argv` in the clear. The command is now marked with
53
+ `secretInput`, so every word typed after it is `[redacted]`. A phone number typed anywhere else, message text included, is replaced by `[redacted]` inside the word,
54
+ the rest kept: one written with its "+" and country code, or 11 to 15 digits in a row, the shapes a kept error
55
+ already masks. A positive id of 11 to 15 digits, such as an 11-digit Zoom meeting id or a time in milliseconds, is
56
+ hidden as well; a negative chat id is kept. A number with no "+" written in groups (`8 912 345-67-89`) is not
57
+ recognised.
58
+
10
59
  ## 0.439.0 — 26.10.2026
11
60
 
12
61
  ### Changed — may break callers
package/README.md CHANGED
@@ -217,8 +217,8 @@ they deliberately differ. A rule changes in its own pull request, before the cod
217
217
  - **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
218
218
  wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
219
219
  choices — even when there is only one.
220
- - **Run records keep ids and timings, never content.** A failure is kept by default, so `runs show` can explain it
221
- without anyone sending a log full of messages.
220
+ - **Run records keep the command as typed, never a secret.** A failure is kept by default, so `runs show` can explain
221
+ it; a token, a password or a phone number is hidden, and `record_input: false` leaves the typed words out too.
222
222
  - **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
223
223
  sleep. A coverage floor: 92% of lines, and at least half of every file.
224
224
  - **Contract cases for every adapter.** `contractCases` turns the port's promises into cases any test runner can run,
@@ -6,6 +6,8 @@ import type { RequestEvent } from "../runs/events.js";
6
6
  export interface SavedRunSettings {
7
7
  record?: boolean | undefined;
8
8
  keepRunsForDays?: number | undefined;
9
+ /** `record_input`: left out, the typed words are kept, secrets hidden. */
10
+ recordInput?: boolean | undefined;
9
11
  }
10
12
  export interface RunSettings {
11
13
  record: boolean;
@@ -59,6 +61,4 @@ export declare const providerRequest: (operation: string, sink?: (event: Request
59
61
  * on. Call it after the program's other `parseAsync` wrappers, so its own is the outermost and sees how the parse
60
62
  * really ended; a refusal in an earlier `preAction` hook is then kept under the words read from the line.
61
63
  */
62
- export declare const recordRuns: (program: Command, options: CommandRunOptions & {
63
- content: string;
64
- }) => void;
64
+ export declare const recordRuns: (program: Command, options: CommandRunOptions) => void;
@@ -2,7 +2,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { isCliError } from "@wirecat/cli-base";
3
3
  import { startRecording, wasSettled } from "../runs/recording.js";
4
4
  import { KEEP_RUNS_FOR_DAYS } from "../runs/run.js";
5
+ import { runContextOf } from "../runs/run-context.js";
5
6
  import { environmentOf } from "./context.js";
7
+ import { rootOf } from "./profile.js";
6
8
  /** `--record`/`--no-record` first, then the config; a failure is kept unless either says no (WC-LIFE-15). */
7
9
  export const runSettings = (flag, saved = {}) => {
8
10
  const record = flag ?? saved.record ?? false;
@@ -41,9 +43,10 @@ const tracker = (options) => {
41
43
  const agent = options.agent ?? environment.agent !== undefined;
42
44
  const streams = options.streams ?? environment.streams;
43
45
  const env = options.env ?? environment.env;
46
+ const command = `${agent ? "mcp " : ""}${words.join(" ") || options.app.command}`;
44
47
  recording = startRecording({
45
48
  app: options.app,
46
- command: `${agent ? "mcp " : ""}${words.join(" ") || options.app.command}`,
49
+ command,
47
50
  ...(profile === undefined ? {} : { profile }),
48
51
  record: settings.record,
49
52
  keepFailed: settings.keepFailedRuns,
@@ -52,6 +55,14 @@ const tracker = (options) => {
52
55
  format,
53
56
  ...(streams ? { streams } : {}),
54
57
  ...(env ? { env } : {}),
58
+ context: runContextOf({
59
+ bin: options.app.command,
60
+ command: words.length > 0 ? command : "",
61
+ program: rootOf(program),
62
+ ...(environment.argv ? { argv: environment.argv } : {}),
63
+ env: env ?? process.env,
64
+ recordInput: saved.recordInput !== false,
65
+ }),
55
66
  });
56
67
  };
57
68
  const finish = () => {
@@ -131,7 +142,7 @@ const isHelpExit = (error) => {
131
142
  */
132
143
  export const recordRuns = (program, options) => {
133
144
  program
134
- .option("--record", `keep this run — ids and timings, never ${options.content}`)
145
+ .option("--record", "keep this run, with the command as typed and secrets hidden; record_input false leaves the typed words out")
135
146
  .option("--no-record", "do not keep this run, even if it fails");
136
147
  program.hook("preAction", (_root, action) => {
137
148
  const parse = parses.getStore();
@@ -24,8 +24,8 @@ export { repliesStatePathFor } from "../replies/state.js";
24
24
  export { type CacheEvent, type DiagnosticEvent, type EventSink, providerErrorKey, type RequestEvent, renderEvent, type WarningEvent, } from "../runs/events.js";
25
25
  export { CORE_LOG_EVENTS } from "../runs/log-events.js";
26
26
  export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
27
- export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
- export { secretInput } from "../runs/run-context.js";
27
+ export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunContext, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
28
+ export { runContextOf, secretInput } from "../runs/run-context.js";
29
29
  export { currentRun, inRun, type RunScope, useAsProcessRun } from "../runs/scope.js";
30
30
  export { LANGUAGES } from "../search/languages.js";
31
31
  export { guardedApp, guardedPress, guardedStart, type Pressed } from "../sends/buttons.js";
package/dist/cli/index.js CHANGED
@@ -21,7 +21,7 @@ export { providerErrorKey, renderEvent, } from "../runs/events.js";
21
21
  export { CORE_LOG_EVENTS } from "../runs/log-events.js";
22
22
  export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
23
23
  export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
24
- export { secretInput } from "../runs/run-context.js";
24
+ export { runContextOf, secretInput } from "../runs/run-context.js";
25
25
  export { currentRun, inRun, useAsProcessRun } from "../runs/scope.js";
26
26
  export { LANGUAGES } from "../search/languages.js";
27
27
  export { guardedApp, guardedPress, guardedStart } from "../sends/buttons.js";
@@ -3,6 +3,7 @@ import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { defineQuery } from "@wirecat/cli-base/query";
4
4
  import { Command } from "commander";
5
5
  import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
6
+ import { secretInput } from "../../runs/run-context.js";
6
7
  import { levelFor } from "../../sends/permissions.js";
7
8
  import { phoneOf } from "../../services/index.js";
8
9
  import { dateWindow } from "../../services/moment.js";
@@ -276,7 +277,7 @@ export const contactsCommand = (messenger) => {
276
277
  * ⚠ **The number is asked for or piped, never an argument**: argv is read by `ps` and kept by shell
277
278
  * history, and a phone number is personal data.
278
279
  */
279
- annotate(contacts.command("lookup"), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
280
+ annotate(secretInput(contacts.command("lookup")), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
280
281
  .summary("find an account by a phone number")
281
282
  .description("who has this phone number — asks for it, or reads it from stdin; never an argument")
282
283
  // Commander's own refusal would repeat the number on stderr.
@@ -69,7 +69,7 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
69
69
  .option("--full", `${found} with their whole text and every provider field`)
70
70
  .option("--dry-run", "preview parsed arguments and permissions before running the action")
71
71
  .option("--yes", "go ahead without the question an ask level puts before a write")
72
- .option("--record", `keep this run — ids and timings, never ${content}`)
72
+ .option("--record", "keep this run, with the command as typed and secrets hidden; record_input false leaves the typed words out")
73
73
  .option("--no-record", "do not keep it, whatever the configuration says")
74
74
  .showHelpAfterError();
75
75
  for (const command of mounted)
@@ -34,6 +34,9 @@ export declare const framesOf: (error: unknown, limit?: number) => string[];
34
34
  * or a token in the path as `:id` / `:token`. The host stays — which service refused is the point.
35
35
  */
36
36
  export declare const endpointTemplate: (raw: string) => string;
37
+ /** A phone number as people type it with its country code, and one written as digits only without the "+". */
38
+ export declare const INTERNATIONAL_PHONE: string;
39
+ export declare const BARE_PHONE: string;
37
40
  /**
38
41
  * An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
39
42
  * phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
@@ -109,6 +109,9 @@ export const endpointTemplate = (raw) => {
109
109
  });
110
110
  return `${scheme}${segments.join("/")}`.slice(0, 300);
111
111
  };
112
+ /** A phone number as people type it with its country code, and one written as digits only without the "+". */
113
+ export const INTERNATIONAL_PHONE = /\+\d[\d\s()-]{6,}\d/.source;
114
+ export const BARE_PHONE = /(^|[^\w-])\d{11,15}\b/.source;
112
115
  /**
113
116
  * An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
114
117
  * phone number, a bot token, a bearer or a long opaque string, a URL's query, the home directory. A second line of
@@ -121,8 +124,8 @@ export const redactText = (text) => {
121
124
  .replace(/"[^"\n]*"|“[^”\n]*”|«[^»\n]*»/g, '"[redacted]"')
122
125
  .replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]\s*|\s+)[^\s'"]+/gi, "$1$2[redacted]")
123
126
  .replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "[email]")
124
- .replace(/\+\d[\d\s()-]{6,}\d/g, "[number]")
125
- .replace(/(^|[^\w-])\d{11,15}\b/g, "$1[number]")
127
+ .replace(new RegExp(INTERNATIONAL_PHONE, "g"), "[number]")
128
+ .replace(new RegExp(BARE_PHONE, "g"), "$1[number]")
126
129
  .replace(/\bbot\d+:[\w-]+/g, "bot[token]")
127
130
  .replace(/\b\d{6,}:[\w-]{20,}\b/g, "[token]")
128
131
  .replace(/(\?|&)[^\s#]*/g, "$1[redacted]")
@@ -22,12 +22,15 @@ export interface MaskedArgv {
22
22
  /** The first word that names no command where one was expected. */
23
23
  unknownCommand?: string;
24
24
  }
25
- /** Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`. */
26
- export declare const secretInput: <T extends Option | Argument>(input: T) => T;
25
+ /**
26
+ * Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`; on a command,
27
+ * every word typed after it that is not an option.
28
+ */
29
+ export declare const secretInput: <T extends Option | Argument | Command>(input: T) => T;
27
30
  /**
28
31
  * The command line for a run record. With `keep` (the owner's `record_input`, on unless turned off) every word stays
29
- * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), and a word shaped
30
- * like a credential. With `keep` off, command words, flag names and the profile stay; a value stays only when its
32
+ * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), a word shaped
33
+ * like a credential, and a phone number inside any word. With `keep` off, command words, flag names and the profile stay; a value stays only when its
31
34
  * shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
32
35
  * option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
33
36
  * else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
@@ -35,7 +38,11 @@ export declare const secretInput: <T extends Option | Argument>(input: T) => T;
35
38
  export declare const maskArgv: (program: Command, words: readonly string[], { keep }?: {
36
39
  keep?: boolean | undefined;
37
40
  }) => MaskedArgv;
38
- /** A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. */
41
+ /**
42
+ * A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. A CLI
43
+ * that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
44
+ * keeps no command line and no agent.
45
+ */
39
46
  export declare const runContextOf: ({ bin, command, program, argv, env, recordInput, }: {
40
47
  bin: string;
41
48
  /** The run's own name, `messages list` or `mcp messages list`; empty when no command was recognised. */
@@ -1,4 +1,5 @@
1
1
  import { isContentKey } from "@wirecat/cli-base/standards";
2
+ import { BARE_PHONE, INTERNATIONAL_PHONE } from "./error-detail.js";
2
3
  export const REDACTED = "[redacted]";
3
4
  /**
4
5
  * The agent is read from what each one sets in the processes it starts: Claude Code `CLAUDECODE` and
@@ -40,7 +41,10 @@ const sessionOf = (kind, env) => {
40
41
  };
41
42
  const plainWord = (word) => /^[A-Za-z][A-Za-z0-9.-]{0,39}$/.test(word);
42
43
  const secretInputs = new WeakSet();
43
- /** Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`. */
44
+ /**
45
+ * Marks an option or argument whose value a run record never keeps, whatever the owner's `record_input`; on a command,
46
+ * every word typed after it that is not an option.
47
+ */
44
48
  export const secretInput = (input) => {
45
49
  secretInputs.add(input);
46
50
  return input;
@@ -67,10 +71,12 @@ const secretNamed = (name) => {
67
71
  };
68
72
  // A password inside a URL, or one unbroken run of 40 token characters, is a credential wherever it was typed.
69
73
  const secretShaped = (word) => /:\/\/[^/@\s:]+:[^/@\s]+@/.test(word) || /(^|[^A-Za-z0-9_-])[A-Za-z0-9_-]{40,}($|[^A-Za-z0-9_-])/.test(word);
74
+ // Messaging CLIs promise a phone number reaches no log, so it goes even from text the record otherwise keeps.
75
+ const withoutPhones = (word) => word.replace(new RegExp(INTERNATIONAL_PHONE, "g"), REDACTED).replace(new RegExp(BARE_PHONE, "g"), `$1${REDACTED}`);
70
76
  /**
71
77
  * The command line for a run record. With `keep` (the owner's `record_input`, on unless turned off) every word stays
72
- * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), and a word shaped
73
- * like a credential. With `keep` off, command words, flag names and the profile stay; a value stays only when its
78
+ * as typed except a secret: a value of an input marked `secretInput` or named for one (`--token`), a word shaped
79
+ * like a credential, and a phone number inside any word. With `keep` off, command words, flag names and the profile stay; a value stays only when its
74
80
  * shape cannot be somebody's words — a number short of a phone's length, a duration, a date, `true`, one of the
75
81
  * option's declared choices — and its option or argument is not named for content (`--text`, `<phone>`). Anything
76
82
  * else, a positional after `--` or an argument no command declares, is `[redacted]`: a chat is often typed by title.
@@ -82,7 +88,7 @@ export const maskArgv = (program, words, { keep = false } = {}) => {
82
88
  let positional = 0;
83
89
  let profileSeen = false;
84
90
  let lost = false;
85
- const raw = (word) => (keep && !secretShaped(word) ? word : REDACTED);
91
+ const raw = (word) => (keep && !secretShaped(word) ? withoutPhones(word) : REDACTED);
86
92
  const keptValue = (name, value, input) => keep
87
93
  ? (input && secretInputs.has(input)) || secretNamed(name)
88
94
  ? REDACTED
@@ -145,7 +151,7 @@ export const maskArgv = (program, words, { keep = false } = {}) => {
145
151
  }
146
152
  if (current === program && !profileSeen) {
147
153
  profileSeen = true;
148
- argv.push(keep ? raw(word) : PROFILE.test(word) ? word : REDACTED);
154
+ argv.push(keep ? raw(word) : PROFILE.test(word) && withoutPhones(word) === word ? word : REDACTED);
149
155
  continue;
150
156
  }
151
157
  if (current.commands.length > 0 && current.registeredArguments.length === 0) {
@@ -158,7 +164,7 @@ export const maskArgv = (program, words, { keep = false } = {}) => {
158
164
  const declared = lost ? undefined : current.registeredArguments;
159
165
  const argument = declared?.[Math.min(positional, declared.length - 1)];
160
166
  positional += 1;
161
- argv.push(argument ? keptValue(argument.name(), word, argument) : raw(word));
167
+ argv.push(secretInputs.has(current) ? REDACTED : argument ? keptValue(argument.name(), word, argument) : raw(word));
162
168
  }
163
169
  return { argv, ...(unknownCommand === undefined ? {} : { unknownCommand }) };
164
170
  };
@@ -184,7 +190,11 @@ const safeShape = (value) => (NUMBER.test(value) && !/^\d{11,}$/.test(value)) ||
184
190
  /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(value) ||
185
191
  value === "true" ||
186
192
  value === "false";
187
- /** A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. */
193
+ /**
194
+ * A run's context from the parsed program: `max messages list` and its words, or the word that named nothing. A CLI
195
+ * that calls `recorded` or `startRecording` for a command of its own passes this as `context`; without it the run
196
+ * keeps no command line and no agent.
197
+ */
188
198
  export const runContextOf = ({ bin, command, program, argv, env, recordInput = false, }) => {
189
199
  const masked = program && argv ? maskArgv(program, argv, { keep: recordInput }) : undefined;
190
200
  const agent = agentOf(env);
@@ -15,17 +15,7 @@ export const TASK_QUERY = {
15
15
  entity: "task",
16
16
  backend: "sql",
17
17
  fields: queryFieldsOf(taskEntity, {
18
- exclude: [
19
- "ref",
20
- "description",
21
- "project",
22
- "account",
23
- "priority",
24
- "parent",
25
- "claimed_by",
26
- "claimed_until",
27
- "close_suggestion",
28
- ],
18
+ exclude: ["ref", "description", "project", "account", "priority", "parent", "claimed_until", "close_suggestion"],
29
19
  relations: {
30
20
  close: {
31
21
  type: "enum",
@@ -47,6 +37,7 @@ export const TASK_QUERY = {
47
37
  chat: "chat:12345",
48
38
  title: "title:*invoice*",
49
39
  source_message: 'source_message:"msg:telegram/1/2/3"',
40
+ claimed_by: "claimed_by:session-1",
50
41
  },
51
42
  }),
52
43
  };
@@ -84,6 +75,13 @@ const taskColumns = (groups, tags) => ({
84
75
  : { sql: "t.source_group = ?", params: [groups.get(node.value) ?? ""] },
85
76
  },
86
77
  source_message: { type: "ref", sql: "t.source_locator" },
78
+ // A lapsed claim holds nothing, as tasks claim and the list's own claimed_by read it.
79
+ claimed_by: {
80
+ where: (node) => ({
81
+ sql: `t.claimed_by ${node.operator === "wildcard" ? "GLOB" : "="} ? AND t.claimed_until >= ?`,
82
+ params: [node.value, Date.now()],
83
+ }),
84
+ },
87
85
  reason: { type: "kw", sql: "t.close_reason_folded", folded: true },
88
86
  close: { where: () => ({ sql: "t.close_suggested_as IS NOT NULL", params: [] }) },
89
87
  assignee: {
@@ -343,16 +341,21 @@ export const tasksService = (deps, categories) => {
343
341
  const { pair } = await store.tasks.unmerge(dup, by === "owner" ? "owner" : { bot: by });
344
342
  return { pair, task: await cardOf(store, dup) };
345
343
  }),
346
- claim: (id, holder, forMs) => inStore(async (store, account) => {
347
- await owned(store, account, id);
344
+ // A project task belongs to no messenger account, as show reads it.
345
+ claim: async (id, holder, forMs) => {
346
+ const store = await deps.store();
347
+ if (!(await store.tasks.projectTask(id)))
348
+ await owned(store, await deps.account(), id);
348
349
  await store.tasks.claim(id, holder, new Date(Date.now() + forMs));
349
350
  return cardOf(store, id);
350
- }),
351
- release: (id, holder) => inStore(async (store, account) => {
352
- await owned(store, account, id);
351
+ },
352
+ release: async (id, holder) => {
353
+ const store = await deps.store();
354
+ if (!(await store.tasks.projectTask(id)))
355
+ await owned(store, await deps.account(), id);
353
356
  await store.tasks.release(id, holder);
354
357
  return cardOf(store, id);
355
- }),
358
+ },
356
359
  close: (id, { as, reason, by, agent, certainty, suggestOnly }) => inStore(async (store, account) => {
357
360
  const existing = await owned(store, account, id);
358
361
  const suggested = existing.state === "open" ? await store.tasks.closeSuggestion(id) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.439.0",
3
+ "version": "0.441.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
package/parity.json CHANGED
@@ -822,7 +822,7 @@
822
822
  "meaning": "why a task was closed, kept with it — no-reply-needed, for example"
823
823
  },
824
824
  "--record": {
825
- "meaning": "keep this run under `runs` — ids and timings, never message content",
825
+ "meaning": "keep this run under `runs`, with the command as typed and secrets hidden; record_input false leaves the typed words out",
826
826
  "note": "max's own copy is worded differently until T6 moves the command onto the shared one (e13)"
827
827
  },
828
828
  "--remind-time": {